Add communication account plugin scaffolds

This commit is contained in:
Anthony Ettinger 2026-06-09 10:02:07 +00:00
parent 5cfeea6b57
commit c23ce42948
48 changed files with 1949 additions and 13 deletions

View file

@ -0,0 +1,15 @@
{
"name": "@logicsrc/account-core",
"version": "0.1.0",
"description": "LogicSRC shared account provider, permission, policy, credential broker, and audit contracts.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "vitest run src"
},
"devDependencies": {
"vitest": "^4.0.8"
}
}

View file

@ -0,0 +1,43 @@
import type { LogicSrcAccountAuditEvent, LogicSrcAccountKind, LogicSrcPolicyDecision, LogicSrcPrincipal } from "./types.js";
const REDACTED_KEYS = new Set(["accessToken", "refreshToken", "token", "password", "secret", "clientSecret", "authorization"]);
export function redactedPreview(input: Record<string, unknown>) {
const output: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input)) {
output[key] = REDACTED_KEYS.has(key) ? "[redacted]" : value;
}
return output;
}
export function createAccountAuditEvent(input: {
id?: string;
accountId?: string;
provider: string;
kind: LogicSrcAccountKind;
principal: LogicSrcPrincipal;
action: string;
decision: LogicSrcPolicyDecision;
riskScore?: number;
requestPreview?: Record<string, unknown>;
resultPreview?: Record<string, unknown>;
correlationId?: string;
createdAt?: string;
}): LogicSrcAccountAuditEvent {
return {
id: input.id ?? `acct_audit_${Date.now()}`,
accountId: input.accountId,
provider: input.provider,
kind: input.kind,
principal: input.principal,
action: input.action,
decision: input.decision,
riskScore: input.riskScore ?? 0,
requestPreview: redactedPreview(input.requestPreview ?? {}),
resultPreview: redactedPreview(input.resultPreview ?? {}),
correlationId: input.correlationId,
createdAt: input.createdAt ?? new Date().toISOString()
};
}

View file

@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { createAccountAuditEvent, createProviderRegistry, evaluateAccountPolicy, isPolicyGatedPermission, riskBandForScore, scoreAccountActionRisk } from "./index.js";
describe("account-core", () => {
it("indexes account providers by kind and id", () => {
const registry = createProviderRegistry([
{ id: "gmail", name: "Gmail", kind: "email", authMethods: ["oauth2"], capabilities: ["email.search"] },
{ id: "mastodon", name: "Mastodon", kind: "social", authMethods: ["oauth2"], capabilities: ["social.post.publish"] }
]);
expect(registry.list("email")).toHaveLength(1);
expect(registry.require("mastodon").name).toBe("Mastodon");
});
it("keeps write and private actions policy-gated", () => {
expect(isPolicyGatedPermission("email:send")).toBe(true);
expect(isPolicyGatedPermission("social:profile:read")).toBe(false);
});
it("scores and bands risky account actions", () => {
const score = scoreAccountActionRisk({ action: "email:send", externalRecipientCount: 1, hasAttachment: true, sensitiveKeywordDetected: true });
expect(score).toBe(0.55);
expect(riskBandForScore(score)).toBe("high");
});
it("requires approval for gated actions with matching grants", () => {
const result = evaluateAccountPolicy({
action: "email:send",
principal: { type: "agent", id: "marketing-agent" },
grant: {
id: "grant_1",
accountId: "account_1",
principal: { type: "agent", id: "marketing-agent" },
permissions: ["email:send"],
policy: [],
createdAt: new Date(0).toISOString()
}
});
expect(result.decision).toBe("approval_required");
});
it("redacts secret-like audit previews", () => {
const event = createAccountAuditEvent({
provider: "gmail",
kind: "email",
principal: { type: "user", id: "user_1" },
action: "accounts:connect",
decision: "allow",
requestPreview: { accessToken: "raw-token", provider: "gmail" }
});
expect(event.requestPreview.accessToken).toBe("[redacted]");
expect(event.requestPreview.provider).toBe("gmail");
});
});

View file

@ -0,0 +1,13 @@
export * from "./types.js";
export {
ACCOUNT_PERMISSIONS,
EMAIL_PERMISSIONS,
POLICY_GATED_PERMISSIONS,
SOCIAL_PERMISSIONS,
SHARED_ACCOUNT_PERMISSIONS,
accountPermissionList,
isPolicyGatedPermission
} from "./permissions.js";
export { createProviderRegistry } from "./provider-registry.js";
export { createAccountAuditEvent, redactedPreview } from "./audit.js";
export { evaluateAccountPolicy, riskBandForScore, scoreAccountActionRisk } from "./policy.js";

View file

@ -0,0 +1,59 @@
export const SHARED_ACCOUNT_PERMISSIONS = [
"accounts:connect",
"accounts:list",
"accounts:read_metadata",
"accounts:test",
"accounts:revoke",
"accounts:sync",
"accounts:audit:read"
] as const;
export const SOCIAL_PERMISSIONS = [
"social:profile:read",
"social:post:draft",
"social:post:publish",
"social:post:delete",
"social:media:upload",
"social:mentions:read",
"social:comments:read",
"social:dm:read",
"social:dm:send",
"social:analytics:read"
] as const;
export const EMAIL_PERMISSIONS = [
"email:headers:read",
"email:body:read",
"email:attachments:read",
"email:search",
"email:draft",
"email:send",
"email:reply",
"email:forward",
"email:archive",
"email:labels:modify",
"email:delete",
"email:sync"
] as const;
export const ACCOUNT_PERMISSIONS = [...SHARED_ACCOUNT_PERMISSIONS, ...SOCIAL_PERMISSIONS, ...EMAIL_PERMISSIONS] as const;
export const POLICY_GATED_PERMISSIONS = [
"social:post:publish",
"social:post:delete",
"social:dm:read",
"social:dm:send",
"email:attachments:read",
"email:send",
"email:delete"
] as const;
export type LogicSrcAccountPermission = (typeof ACCOUNT_PERMISSIONS)[number];
export function accountPermissionList() {
return [...ACCOUNT_PERMISSIONS];
}
export function isPolicyGatedPermission(permission: string) {
return (POLICY_GATED_PERMISSIONS as readonly string[]).includes(permission);
}

View file

@ -0,0 +1,92 @@
import { isPolicyGatedPermission } from "./permissions.js";
import type { LogicSrcPolicyEvaluationInput, LogicSrcPolicyEvaluationResult, LogicSrcRiskBand } from "./types.js";
const WRITE_ACTIONS = new Set([
"social:post:publish",
"social:post:delete",
"social:dm:send",
"email:send",
"email:reply",
"email:forward",
"email:delete",
"email:labels:modify",
"email:archive"
]);
export function riskBandForScore(score: number): LogicSrcRiskBand {
if (score >= 0.75) {
return "critical";
}
if (score >= 0.5) {
return "high";
}
if (score >= 0.25) {
return "medium";
}
return "low";
}
export function scoreAccountActionRisk(input: {
action: string;
externalRecipientCount?: number;
newRecipientOrDomain?: boolean;
hasAttachment?: boolean;
highReachAccount?: boolean;
sensitiveKeywordDetected?: boolean;
rawCredentialAccessAttempted?: boolean;
}) {
let score = 0;
if (input.externalRecipientCount && input.externalRecipientCount > 0) score += 0.1;
if (input.newRecipientOrDomain) score += 0.15;
if (input.hasAttachment) score += 0.15;
if (input.action === "social:post:publish") score += 0.2;
if (input.action.includes("delete")) score += 0.2;
if (input.action.includes("dm:read") || input.action.includes("attachments:read")) score += 0.25;
if (input.highReachAccount) score += 0.25;
if (input.sensitiveKeywordDetected) score += 0.3;
if (input.rawCredentialAccessAttempted) score += 0.4;
return Math.min(1, Number(score.toFixed(2)));
}
export function evaluateAccountPolicy(input: LogicSrcPolicyEvaluationInput): LogicSrcPolicyEvaluationResult {
const riskScore = Math.min(1, Math.max(0, input.riskScore ?? scoreAccountActionRisk({ action: input.action })));
const grantActive = input.grant && !input.grant.revokedAt && (!input.grant.expiresAt || Date.parse(input.grant.expiresAt) > Date.now());
const hasPermission = Boolean(grantActive && input.grant?.permissions.includes(input.action));
if (!hasPermission) {
return { decision: "deny", riskScore, reason: `missing grant for ${input.action}` };
}
if (input.dryRun) {
return { decision: "allow", riskScore, reason: "dry run with matching grant" };
}
const policy = input.grant?.policy.find((entry) => entry.action === input.action);
if (policy?.default === "deny") {
return { decision: "deny", riskScore, reason: `policy ${policy.id} denies ${input.action}` };
}
if (policy?.default === "approval_required") {
return { decision: "approval_required", riskScore, reason: `policy ${policy.id} requires approval` };
}
if (policy?.default === "allow_if_trusted_agent" && input.principal?.trusted) {
return { decision: "allow", riskScore, reason: `policy ${policy.id} allows trusted principal` };
}
if (policy?.default === "allow_if_below_risk_score") {
const maxRiskScore = typeof policy.conditions?.maxRiskScore === "number" ? policy.conditions.maxRiskScore : 0.25;
return riskScore <= maxRiskScore
? { decision: "allow", riskScore, reason: `risk score is within policy ${policy.id}` }
: { decision: "approval_required", riskScore, reason: `risk score exceeds policy ${policy.id}` };
}
const band = riskBandForScore(riskScore);
if (band === "critical") {
return { decision: "deny", riskScore, reason: "critical risk requires admin override" };
}
if (isPolicyGatedPermission(input.action) || (WRITE_ACTIONS.has(input.action) && band !== "low")) {
return { decision: "approval_required", riskScore, reason: "default gate for risky account action" };
}
return { decision: "allow", riskScore, reason: "grant allows account action" };
}

View file

@ -0,0 +1,28 @@
import type { LogicSrcAccountKind, LogicSrcAccountProviderManifest } from "./types.js";
export function createProviderRegistry(providers: LogicSrcAccountProviderManifest[]) {
const byId = new Map<string, LogicSrcAccountProviderManifest>();
for (const provider of providers) {
if (byId.has(provider.id)) {
throw new Error(`Duplicate account provider: ${provider.id}`);
}
byId.set(provider.id, provider);
}
return {
list(kind?: LogicSrcAccountKind) {
return providers.filter((provider) => !kind || provider.kind === kind);
},
get(id: string) {
return byId.get(id);
},
require(id: string) {
const provider = byId.get(id);
if (!provider) {
throw new Error(`Unknown account provider: ${id}`);
}
return provider;
}
};
}

View file

@ -0,0 +1,447 @@
export type LogicSrcAccountKind = "social" | "email";
export type LogicSrcAccountStatus = "connected" | "expired" | "revoked" | "error" | "disabled" | "pending";
export type LogicSrcAccountAuthMethod = "oauth2" | "api_key" | "imap_smtp" | "local_bridge";
export type LogicSrcPrincipalType = "user" | "agent" | "workflow" | "plugin";
export type LogicSrcPolicyMode =
| "allow"
| "approval_required"
| "deny"
| "allow_if_dry_run"
| "allow_if_trusted_agent"
| "allow_if_below_risk_score";
export type LogicSrcPolicyDecision = "allow" | "approval_required" | "deny";
export type LogicSrcRiskBand = "low" | "medium" | "high" | "critical";
export interface LogicSrcConnectedAccount {
id: string;
orgId?: string;
projectId?: string;
boardId?: string;
ownerUserId: string;
kind: LogicSrcAccountKind;
provider: string;
providerAccountId?: string;
displayName: string;
handle?: string;
email?: string;
avatarUrl?: string;
homepageUrl?: string;
status: LogicSrcAccountStatus;
scopes: string[];
capabilities: string[];
credentialRef: string;
metadata: Record<string, unknown>;
createdAt: string;
updatedAt: string;
lastSyncedAt?: string;
}
export interface LogicSrcAccountProviderManifest {
id: string;
name: string;
kind: LogicSrcAccountKind;
authMethods: LogicSrcAccountAuthMethod[];
capabilities: string[];
defaultScopes?: string[];
status?: "available" | "planned" | "disabled";
docsUrl?: string;
}
export interface LogicSrcAccountProvider extends LogicSrcAccountProviderManifest {
getAuthUrl?(input: AuthUrlInput): Promise<AuthUrlResult>;
completeAuth?(input: CompleteAuthInput): Promise<ConnectedAccountResult>;
refreshCredential?(input: RefreshCredentialInput): Promise<CredentialRefreshResult>;
testConnection(input: TestConnectionInput): Promise<TestConnectionResult>;
revoke?(input: RevokeAccountInput): Promise<RevokeAccountResult>;
}
export interface SocialAccountProvider extends LogicSrcAccountProvider {
kind: "social";
getProfile(input: SocialAccountInput): Promise<SocialProfile>;
draftPost(input: DraftSocialPostInput): Promise<SocialDraft>;
publishPost(input: PublishSocialPostInput): Promise<PublishedSocialPost>;
uploadMedia?(input: UploadSocialMediaInput): Promise<UploadedMedia>;
searchMentions?(input: SearchMentionsInput): Promise<SocialMention[]>;
listComments?(input: ListCommentsInput): Promise<SocialComment[]>;
getAnalytics?(input: SocialAnalyticsInput): Promise<SocialAnalyticsResult>;
}
export interface EmailAccountProvider extends LogicSrcAccountProvider {
kind: "email";
searchMessages(input: EmailSearchInput): Promise<EmailSearchResult>;
readMessage(input: ReadEmailMessageInput): Promise<EmailMessage>;
draftMessage(input: DraftEmailInput): Promise<EmailDraft>;
sendMessage(input: SendEmailInput): Promise<SentEmailResult>;
replyToMessage?(input: ReplyEmailInput): Promise<SentEmailResult>;
forwardMessage?(input: ForwardEmailInput): Promise<SentEmailResult>;
archiveMessage?(input: EmailMessageMutationInput): Promise<EmailMutationResult>;
applyLabels?(input: EmailLabelInput): Promise<EmailMutationResult>;
deleteMessage?(input: EmailMessageMutationInput): Promise<EmailMutationResult>;
}
export interface AuthUrlInput {
redirectUri: string;
state: string;
scopes: string[];
metadata?: Record<string, unknown>;
}
export interface AuthUrlResult {
url: string;
state: string;
expiresAt?: string;
}
export interface CompleteAuthInput {
code: string;
redirectUri: string;
state: string;
principal: LogicSrcPrincipal;
}
export interface ConnectedAccountResult {
account: LogicSrcConnectedAccount;
auditEvent: LogicSrcAccountAuditEvent;
}
export interface RefreshCredentialInput {
account: LogicSrcConnectedAccount;
broker: LogicSrcCredentialBroker;
}
export interface CredentialRefreshResult {
credentialRef: string;
scopes: string[];
expiresAt?: string;
}
export interface TestConnectionInput {
account: LogicSrcConnectedAccount;
broker: LogicSrcCredentialBroker;
}
export interface TestConnectionResult {
ok: boolean;
provider: string;
checkedAt: string;
message?: string;
}
export interface RevokeAccountInput {
account: LogicSrcConnectedAccount;
broker: LogicSrcCredentialBroker;
principal: LogicSrcPrincipal;
}
export interface RevokeAccountResult {
ok: boolean;
revokedAt: string;
}
export interface LogicSrcPrincipal {
type: LogicSrcPrincipalType;
id: string;
trusted?: boolean;
}
export interface LogicSrcAccountPermissionGrant {
id: string;
accountId: string;
principal: LogicSrcPrincipal;
permissions: string[];
policy: LogicSrcAccountPolicy[];
expiresAt?: string;
createdBy?: string;
createdAt: string;
revokedAt?: string;
}
export interface LogicSrcAccountPolicy {
id: string;
resource: string;
action: string;
default: LogicSrcPolicyMode;
conditions?: Record<string, unknown>;
}
export interface LogicSrcPolicyEvaluationInput {
action: string;
grant?: LogicSrcAccountPermissionGrant;
riskScore?: number;
dryRun?: boolean;
principal?: LogicSrcPrincipal;
}
export interface LogicSrcPolicyEvaluationResult {
decision: LogicSrcPolicyDecision;
riskScore: number;
reason: string;
}
export interface LogicSrcCredentialBroker {
getCredential(input: CredentialBrokerGetInput): Promise<CredentialBrokerGetResult>;
storeCredential(input: CredentialBrokerStoreInput): Promise<CredentialBrokerStoreResult>;
revokeCredential(input: CredentialBrokerRevokeInput): Promise<CredentialBrokerRevokeResult>;
}
export interface CredentialBrokerGetInput {
credentialRef: string;
accountId: string;
provider: string;
purpose: string;
principal: LogicSrcPrincipal;
}
export interface CredentialBrokerGetResult {
credentialRef: string;
token: string;
expiresAt?: string;
}
export interface CredentialBrokerStoreInput {
provider: string;
kind: LogicSrcAccountKind;
scopes: string[];
secret: string;
metadata?: Record<string, unknown>;
}
export interface CredentialBrokerStoreResult {
credentialRef: string;
}
export interface CredentialBrokerRevokeInput {
credentialRef: string;
accountId: string;
provider: string;
principal: LogicSrcPrincipal;
}
export interface CredentialBrokerRevokeResult {
ok: boolean;
revokedAt: string;
}
export interface LogicSrcAccountAuditEvent {
id: string;
accountId?: string;
provider: string;
kind: LogicSrcAccountKind;
principal: LogicSrcPrincipal;
action: string;
decision: LogicSrcPolicyDecision;
riskScore: number;
requestPreview: Record<string, unknown>;
resultPreview: Record<string, unknown>;
correlationId?: string;
createdAt: string;
}
export interface SocialAccountInput {
account: LogicSrcConnectedAccount;
broker: LogicSrcCredentialBroker;
}
export interface SocialProfile {
providerAccountId: string;
displayName: string;
handle?: string;
avatarUrl?: string;
homepageUrl?: string;
metadata?: Record<string, unknown>;
}
export interface DraftSocialPostInput extends SocialAccountInput {
text: string;
media?: SocialMediaInput[];
principal: LogicSrcPrincipal;
}
export interface PublishSocialPostInput extends DraftSocialPostInput {
dryRun?: boolean;
approvalId?: string;
}
export interface SocialDraft {
id: string;
accountId: string;
text: string;
media: SocialMediaInput[];
createdAt: string;
}
export interface PublishedSocialPost {
providerPostId?: string;
url?: string;
publishedAt: string;
dryRun?: boolean;
}
export interface SocialMediaInput {
url?: string;
fileRef?: string;
altText?: string;
mimeType?: string;
}
export interface UploadSocialMediaInput extends SocialAccountInput {
media: SocialMediaInput;
}
export interface UploadedMedia {
providerMediaId: string;
url?: string;
}
export interface SearchMentionsInput extends SocialAccountInput {
query?: string;
limit?: number;
}
export interface SocialMention {
id: string;
text: string;
authorHandle?: string;
url?: string;
createdAt?: string;
}
export interface ListCommentsInput extends SocialAccountInput {
postId: string;
limit?: number;
}
export interface SocialComment extends SocialMention {
postId: string;
}
export interface SocialAnalyticsInput extends SocialAccountInput {
postId?: string;
since?: string;
until?: string;
}
export interface SocialAnalyticsResult {
metrics: Record<string, number>;
metadata?: Record<string, unknown>;
}
export interface EmailSearchInput {
account: LogicSrcConnectedAccount;
broker: LogicSrcCredentialBroker;
query: string;
limit?: number;
headersOnly?: boolean;
principal: LogicSrcPrincipal;
}
export interface EmailSearchResult {
messages: EmailMessageMetadata[];
nextPageToken?: string;
}
export interface EmailMessageMetadata {
id: string;
providerMessageId: string;
threadId?: string;
subject?: string;
fromAddress?: string;
toAddresses: string[];
ccAddresses: string[];
snippet?: string;
labels: string[];
hasAttachments: boolean;
receivedAt?: string;
sentAt?: string;
}
export interface ReadEmailMessageInput {
account: LogicSrcConnectedAccount;
broker: LogicSrcCredentialBroker;
messageId: string;
includeBody?: boolean;
includeAttachments?: boolean;
principal: LogicSrcPrincipal;
}
export interface EmailMessage extends EmailMessageMetadata {
bodyText?: string;
bodyHtml?: string;
attachments?: EmailAttachment[];
}
export interface EmailAttachment {
id: string;
filename: string;
mimeType: string;
sizeBytes?: number;
contentRef?: string;
}
export interface DraftEmailInput {
account: LogicSrcConnectedAccount;
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
bodyText?: string;
bodyHtml?: string;
attachmentRefs?: string[];
principal: LogicSrcPrincipal;
}
export interface EmailDraft {
id: string;
accountId: string;
to: string[];
cc: string[];
bcc: string[];
subject: string;
bodyPreview: string;
attachmentRefs: string[];
createdAt: string;
}
export interface SendEmailInput extends DraftEmailInput {
draftId?: string;
dryRun?: boolean;
approvalId?: string;
}
export interface SentEmailResult {
providerMessageId?: string;
sentAt: string;
dryRun?: boolean;
}
export interface ReplyEmailInput extends SendEmailInput {
messageId: string;
}
export interface ForwardEmailInput extends SendEmailInput {
messageId: string;
}
export interface EmailMessageMutationInput {
account: LogicSrcConnectedAccount;
broker: LogicSrcCredentialBroker;
messageId: string;
principal: LogicSrcPrincipal;
}
export interface EmailLabelInput extends EmailMessageMutationInput {
add?: string[];
remove?: string[];
}
export interface EmailMutationResult {
ok: boolean;
messageId: string;
changedAt: string;
}

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}

View file

@ -14,9 +14,12 @@
"test": "vitest run src"
},
"dependencies": {
"@logicsrc/account-core": "file:../account-core",
"@logicsrc/plugin-core": "file:../plugin-core",
"@logicsrc/plugin-coinpay": "file:../../plugins/coinpay",
"@logicsrc/plugin-email-accounts": "file:../../plugins/email-accounts",
"@logicsrc/plugin-feed-discovery": "file:../../plugins/feed-discovery",
"@logicsrc/plugin-social-accounts": "file:../../plugins/social-accounts",
"@logicsrc/plugin-ugig": "file:../../plugins/ugig",
"@logicsrc/tui": "file:../tui",
"@logicsrc/validators": "file:../validators",

View file

@ -1,7 +1,10 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { evaluateAccountPolicy, scoreAccountActionRisk } from "@logicsrc/account-core";
import { Command } from "commander";
import { listEmailAccountProviders } from "@logicsrc/plugin-email-accounts";
import { discoverFeeds, listFeedProviders, probeSite, renderDiscoveryOutput, validateFeed, type FeedKind, type FeedOutputFormat } from "@logicsrc/plugin-feed-discovery";
import { listSocialAccountProviders } from "@logicsrc/plugin-social-accounts";
import { renderArcadeList, renderPluginStatus, renderTui, runArcadeSession, type TaskSnapshot } from "@logicsrc/tui";
import { assertSchemaKind, parseDocument, validate } from "@logicsrc/validators";
import { getConfigValue, readConfig, setConfigValue, writeConfig } from "./config.js";
@ -399,6 +402,132 @@ credentials.command("plan").option("--from <provider>", "Source provider", "env"
);
});
const accounts = program.command("accounts").description("Manage connected social and email accounts.");
accounts
.command("providers")
.option("--kind <kind>", "social or email")
.option("--format <format>", "table, json, or markdown", "table")
.description("List communication account providers.")
.action((options) => {
const providers = [...listSocialAccountProviders(), ...listEmailAccountProviders()].filter((provider) => !options.kind || provider.kind === options.kind);
print(providers, options.format as OutputFormat);
});
accounts
.command("list")
.alias("accounts")
.option("--kind <kind>", "social or email")
.option("--format <format>", "table, json, or markdown", "table")
.description("List connected accounts.")
.action((options) => {
print([], options.format as OutputFormat);
});
accounts
.command("audit")
.argument("<account-id>", "Connected account id")
.option("--format <format>", "table, json, or markdown", "table")
.description("List account audit events.")
.action((accountId, options) => {
print({ account_id: accountId, events: [], note: "Account audit persistence is not wired yet." }, options.format as OutputFormat);
});
const social = program.command("social").description("Manage social account providers and draft/publish flows.");
social.command("providers").option("--format <format>", "table, json, or markdown", "table").description("List social account providers.").action((options) => {
print(listSocialAccountProviders(), options.format as OutputFormat);
});
social.command("accounts").option("--format <format>", "table, json, or markdown", "table").description("List connected social accounts.").action((options) => {
print([], options.format as OutputFormat);
});
social
.command("post")
.argument("<account-id>", "Connected social account id")
.requiredOption("--text <text>", "Post text")
.option("--dry-run", "Evaluate without publishing", false)
.option("--format <format>", "table, json, or markdown", "table")
.description("Request or dry-run a social post publish.")
.action((accountId, options) => {
const riskScore = scoreAccountActionRisk({ action: "social:post:publish" });
const decision = evaluateAccountPolicy({
action: "social:post:publish",
dryRun: Boolean(options.dryRun),
riskScore,
grant: {
id: "dry_run_grant",
accountId,
principal: { type: "user", id: process.env.COMMANDBOARD_DID || "local-user" },
permissions: ["social:post:publish"],
policy: [],
createdAt: new Date(0).toISOString()
}
});
print(
{
provider: "unknown",
account_id: accountId,
action: "social:post:publish",
dry_run: Boolean(options.dryRun),
scopes_required: ["social:post:publish"],
policy_decision: decision.decision,
risk_score: decision.riskScore,
payload_preview: { text: options.text }
},
options.format as OutputFormat
);
});
const email = program.command("email").description("Manage email account providers and draft/send flows.");
email.command("providers").option("--format <format>", "table, json, or markdown", "table").description("List email account providers.").action((options) => {
print(listEmailAccountProviders(), options.format as OutputFormat);
});
email.command("accounts").option("--format <format>", "table, json, or markdown", "table").description("List connected email accounts.").action((options) => {
print([], options.format as OutputFormat);
});
email
.command("send")
.argument("<draft-id>", "Email draft id")
.option("--dry-run", "Evaluate without sending", false)
.option("--format <format>", "table, json, or markdown", "table")
.description("Request or dry-run an outbound email send.")
.action((draftId, options) => {
const riskScore = scoreAccountActionRisk({ action: "email:send", externalRecipientCount: 1 });
const decision = evaluateAccountPolicy({
action: "email:send",
dryRun: Boolean(options.dryRun),
riskScore,
grant: {
id: "dry_run_grant",
accountId: "unknown",
principal: { type: "user", id: process.env.COMMANDBOARD_DID || "local-user" },
permissions: ["email:send"],
policy: [],
createdAt: new Date(0).toISOString()
}
});
print(
{
provider: "unknown",
draft_id: draftId,
action: "email:send",
dry_run: Boolean(options.dryRun),
scopes_required: ["email:send"],
policy_decision: decision.decision,
risk_score: decision.riskScore,
payload_preview: { draft_id: draftId }
},
options.format as OutputFormat
);
});
program.command("tui").description("Launch the tmux-friendly TUI.").action(() => {
console.log(renderTui());
console.log("\nPlugin status:\n" + renderPluginStatus());

View file

@ -1,8 +1,10 @@
import { createPluginRegistry } from "@logicsrc/plugin-core";
import { coinPayPlugin } from "@logicsrc/plugin-coinpay";
import { emailAccountsPlugin } from "@logicsrc/plugin-email-accounts";
import { feedDiscoveryPlugin } from "@logicsrc/plugin-feed-discovery";
import { socialAccountsPlugin } from "@logicsrc/plugin-social-accounts";
import { uGigPlugin } from "@logicsrc/plugin-ugig";
export function defaultPluginRegistry() {
return createPluginRegistry([coinPayPlugin, uGigPlugin, feedDiscoveryPlugin]);
return createPluginRegistry([coinPayPlugin, uGigPlugin, feedDiscoveryPlugin, socialAccountsPlugin, emailAccountsPlugin]);
}

View file

@ -3,6 +3,7 @@ import { z } from "zod";
import { assertSchemaKind, parseDocument, schemas, validate, type SchemaKind } from "@logicsrc/validators";
const docs = {
"communication-accounts": `LogicSRC Communication Accounts defines shared contracts for connecting social and email identities, granting scoped human/agent/plugin access, evaluating policy gates, brokering credentials, and auditing every account action without exposing raw secrets.`,
positioning: `LogicSRC is an open standards initiative for human and AI agent coordination, maintained by Profullstack, Inc.
CommandBoard.run is a hosted product by Profullstack, Inc., built on LogicSRC. LogicSRC defines identity, boards, posts, tasks, bounties, agents, agent runs, permissions, payments, escrow, reputation, events, webhooks, CLI commands, API schemas, and plugin contracts.`,
@ -80,7 +81,7 @@ export function createLogicSrcMcpServer() {
title: "Validate LogicSRC Document",
description: "Validates a JSON or YAML document against a LogicSRC schema kind.",
inputSchema: {
kind: z.enum(["agent", "event", "plugin", "run", "task"]),
kind: z.enum(["account-audit-event", "account-grant", "account-provider", "agent", "connected-account", "email-message", "event", "plugin", "run", "social-post", "task"]),
document: z.string().describe("JSON or YAML document text."),
fileName: z.string().optional().describe("Optional file name used to select JSON parsing when it ends with .json.")
},
@ -99,7 +100,7 @@ export function createLogicSrcMcpServer() {
title: "Generate Example LogicSRC Document",
description: "Returns a minimal example document for a LogicSRC schema kind.",
inputSchema: {
kind: z.enum(["agent", "event", "plugin", "run", "task"])
kind: z.enum(["account-audit-event", "account-grant", "account-provider", "agent", "connected-account", "email-message", "event", "plugin", "run", "social-post", "task"])
},
annotations: { readOnlyHint: true, openWorldHint: false }
},
@ -163,6 +164,36 @@ function titleCase(value: string) {
function exampleFor(kind: SchemaKind) {
switch (kind) {
case "account-audit-event":
return {
id: "acct_audit_123",
provider: "gmail",
kind: "email",
principal: { type: "agent", id: "marketing-agent" },
action: "email:send",
decision: "approval_required",
riskScore: 0.35,
requestPreview: { draft_id: "draft_123" },
resultPreview: {},
createdAt: new Date(0).toISOString()
};
case "account-grant":
return {
id: "grant_123",
accountId: "account_123",
principal: { type: "agent", id: "marketing-agent" },
permissions: ["email:headers:read", "email:draft"],
policy: [],
createdAt: new Date(0).toISOString()
};
case "account-provider":
return {
id: "gmail",
name: "Gmail",
kind: "email",
authMethods: ["oauth2"],
capabilities: ["email.headers.read", "email.search"]
};
case "agent":
return {
type: "logicsrc.agent",
@ -172,6 +203,32 @@ function exampleFor(kind: SchemaKind) {
capabilities: ["browser.qa", "report.write"],
status: "active"
};
case "connected-account":
return {
id: "account_123",
ownerUserId: "user_123",
kind: "email",
provider: "gmail",
displayName: "Founder Inbox",
email: "founder@example.com",
status: "connected",
scopes: ["gmail.metadata"],
capabilities: ["email.headers.read", "email.search"],
credentialRef: "cred://gmail/account_123",
metadata: {},
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString()
};
case "email-message":
return {
id: "email_msg_123",
providerMessageId: "provider_msg_123",
subject: "Hello",
toAddresses: ["founder@example.com"],
ccAddresses: [],
labels: ["inbox"],
hasAttachments: false
};
case "event":
return {
type: "logicsrc.event",
@ -203,6 +260,17 @@ function exampleFor(kind: SchemaKind) {
status: "completed",
started_at: new Date(0).toISOString()
};
case "social-post":
return {
id: "social_post_123",
accountId: "account_123",
status: "draft",
text: "Launching today",
media: [],
metadata: {},
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString()
};
case "task":
return {
type: "logicsrc.task",

View file

@ -8,7 +8,13 @@
"./agent": "./schemas/logicsrc-agent.schema.json",
"./run": "./schemas/logicsrc-run.schema.json",
"./event": "./schemas/logicsrc-event.schema.json",
"./plugin": "./schemas/logicsrc-plugin.schema.json"
"./plugin": "./schemas/logicsrc-plugin.schema.json",
"./connected-account": "./schemas/logicsrc-connected-account.schema.json",
"./account-provider": "./schemas/logicsrc-account-provider.schema.json",
"./account-grant": "./schemas/logicsrc-account-grant.schema.json",
"./account-audit-event": "./schemas/logicsrc-account-audit-event.schema.json",
"./email-message": "./schemas/logicsrc-email-message.schema.json",
"./social-post": "./schemas/logicsrc-social-post.schema.json"
},
"files": [
"schemas"

View file

@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-account-audit-event.schema.json",
"title": "LogicSRC Account Audit Event",
"type": "object",
"required": ["id", "provider", "kind", "principal", "action", "decision", "riskScore", "requestPreview", "resultPreview", "createdAt"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"accountId": { "type": "string", "minLength": 1 },
"provider": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
"kind": { "enum": ["social", "email"] },
"principal": {
"type": "object",
"required": ["type", "id"],
"additionalProperties": false,
"properties": {
"type": { "enum": ["user", "agent", "workflow", "plugin"] },
"id": { "type": "string", "minLength": 1 },
"trusted": { "type": "boolean" }
}
},
"action": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)+$" },
"decision": { "enum": ["allow", "approval_required", "deny"] },
"riskScore": { "type": "number", "minimum": 0, "maximum": 1 },
"requestPreview": { "type": "object" },
"resultPreview": { "type": "object" },
"correlationId": { "type": "string", "minLength": 1 },
"createdAt": { "type": "string", "format": "date-time" }
}
}

View file

@ -0,0 +1,48 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-account-grant.schema.json",
"title": "LogicSRC Account Permission Grant",
"type": "object",
"required": ["id", "accountId", "principal", "permissions", "policy", "createdAt"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"accountId": { "type": "string", "minLength": 1 },
"principal": {
"type": "object",
"required": ["type", "id"],
"additionalProperties": false,
"properties": {
"type": { "enum": ["user", "agent", "workflow", "plugin"] },
"id": { "type": "string", "minLength": 1 },
"trusted": { "type": "boolean" }
}
},
"permissions": {
"type": "array",
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)+$" },
"uniqueItems": true
},
"policy": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "resource", "action", "default"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"resource": { "type": "string", "minLength": 1 },
"action": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)+$" },
"default": {
"enum": ["allow", "approval_required", "deny", "allow_if_dry_run", "allow_if_trusted_agent", "allow_if_below_risk_score"]
},
"conditions": { "type": "object" }
}
}
},
"expiresAt": { "type": "string", "format": "date-time" },
"createdBy": { "type": "string", "minLength": 1 },
"createdAt": { "type": "string", "format": "date-time" },
"revokedAt": { "type": "string", "format": "date-time" }
}
}

View file

@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-account-provider.schema.json",
"title": "LogicSRC Account Provider",
"type": "object",
"required": ["id", "name", "kind", "authMethods", "capabilities"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
"name": { "type": "string", "minLength": 1 },
"kind": { "enum": ["social", "email"] },
"authMethods": {
"type": "array",
"items": { "enum": ["oauth2", "api_key", "imap_smtp", "local_bridge"] },
"minItems": 1,
"uniqueItems": true
},
"capabilities": {
"type": "array",
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$" },
"uniqueItems": true
},
"defaultScopes": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
},
"status": { "enum": ["available", "planned", "disabled"] },
"docsUrl": { "type": "string", "format": "uri" }
}
}

View file

@ -0,0 +1,39 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-connected-account.schema.json",
"title": "LogicSRC Connected Account",
"type": "object",
"required": ["id", "ownerUserId", "kind", "provider", "displayName", "status", "scopes", "capabilities", "credentialRef", "metadata", "createdAt", "updatedAt"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"orgId": { "type": "string", "minLength": 1 },
"projectId": { "type": "string", "minLength": 1 },
"boardId": { "type": "string", "minLength": 1 },
"ownerUserId": { "type": "string", "minLength": 1 },
"kind": { "enum": ["social", "email"] },
"provider": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
"providerAccountId": { "type": "string", "minLength": 1 },
"displayName": { "type": "string", "minLength": 1 },
"handle": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"avatarUrl": { "type": "string", "format": "uri" },
"homepageUrl": { "type": "string", "format": "uri" },
"status": { "enum": ["connected", "expired", "revoked", "error", "disabled", "pending"] },
"scopes": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
},
"capabilities": {
"type": "array",
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$" },
"uniqueItems": true
},
"credentialRef": { "type": "string", "minLength": 1 },
"metadata": { "type": "object" },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" },
"lastSyncedAt": { "type": "string", "format": "date-time" }
}
}

View file

@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-email-message.schema.json",
"title": "LogicSRC Email Message Metadata",
"type": "object",
"required": ["id", "providerMessageId", "toAddresses", "ccAddresses", "labels", "hasAttachments"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"providerMessageId": { "type": "string", "minLength": 1 },
"threadId": { "type": "string", "minLength": 1 },
"subject": { "type": "string" },
"fromAddress": { "type": "string" },
"toAddresses": { "type": "array", "items": { "type": "string" } },
"ccAddresses": { "type": "array", "items": { "type": "string" } },
"snippet": { "type": "string" },
"labels": { "type": "array", "items": { "type": "string" } },
"hasAttachments": { "type": "boolean" },
"receivedAt": { "type": "string", "format": "date-time" },
"sentAt": { "type": "string", "format": "date-time" }
}
}

View file

@ -0,0 +1,26 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-social-post.schema.json",
"title": "LogicSRC Social Post",
"type": "object",
"required": ["id", "accountId", "status", "media", "metadata", "createdAt", "updatedAt"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"accountId": { "type": "string", "minLength": 1 },
"providerPostId": { "type": "string", "minLength": 1 },
"status": { "enum": ["draft", "pending_approval", "published", "deleted", "failed"] },
"text": { "type": "string" },
"media": {
"type": "array",
"items": { "type": "object" }
},
"url": { "type": "string", "format": "uri" },
"publishedAt": { "type": "string", "format": "date-time" },
"createdByPrincipalType": { "enum": ["user", "agent", "workflow", "plugin"] },
"createdByPrincipalId": { "type": "string", "minLength": 1 },
"metadata": { "type": "object" },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" }
}
}

View file

@ -11,6 +11,8 @@
"dependencies": {
"@logicsrc/plugin-core": "file:../plugin-core",
"@logicsrc/plugin-coinpay": "file:../../plugins/coinpay",
"@logicsrc/plugin-email-accounts": "file:../../plugins/email-accounts",
"@logicsrc/plugin-social-accounts": "file:../../plugins/social-accounts",
"@logicsrc/plugin-ugig": "file:../../plugins/ugig"
}
}

View file

@ -1,5 +1,7 @@
import { createPluginRegistry } from "@logicsrc/plugin-core";
import { coinPayPlugin } from "@logicsrc/plugin-coinpay";
import { emailAccountsPlugin } from "@logicsrc/plugin-email-accounts";
import { socialAccountsPlugin } from "@logicsrc/plugin-social-accounts";
import { uGigPlugin } from "@logicsrc/plugin-ugig";
export { ArcadeRegistry, createDefaultArcadeRegistry, renderArcadeList, renderArcadeSnapshot, runArcadeSession } from "./arcade/index.js";
export type { ArcadeEvent, GameAction, GameContext, GameControl, KeyEvent, TaskEvent, TaskSnapshot, TerminalFrame, WaitingGame } from "./arcade/index.js";
@ -20,7 +22,7 @@ const defaultState: TuiState = {
export function renderTui(state: Partial<TuiState> = {}) {
const view = { ...defaultState, ...state };
const registry = createPluginRegistry([coinPayPlugin, uGigPlugin]);
const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, socialAccountsPlugin, emailAccountsPlugin]);
const plugins = registry.snapshot().plugins;
return [
@ -34,13 +36,13 @@ export function renderTui(state: Partial<TuiState> = {}) {
"│ /jobs │ [uGig] Senior AI Engineer remote │",
"├───────────────┴─────────────────────────────────────────────┤",
"│ Plugins: " + plugins.map((plugin) => `${plugin.name} ${plugin.enabled ? "enabled" : "disabled"}`).join(" | ").padEnd(50) + " │",
"│ Enter: open p: post t: task a: agents w: wallet q: quit │",
"│ Enter: open p: post t: task a: agents c: accounts q: quit │",
"└─────────────────────────────────────────────────────────────┘"
].join("\n");
}
export function renderPluginStatus() {
const registry = createPluginRegistry([coinPayPlugin, uGigPlugin]);
const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, socialAccountsPlugin, emailAccountsPlugin]);
return registry
.snapshot()
.plugins.map((plugin) => `${plugin.id.padEnd(8)} ${plugin.enabled ? "enabled" : "disabled"} ${plugin.type.join(", ")}`)

View file

@ -1,14 +1,26 @@
import agentSchema from "../../schemas/schemas/logicsrc-agent.schema.json" with { type: "json" };
import accountAuditEventSchema from "../../schemas/schemas/logicsrc-account-audit-event.schema.json" with { type: "json" };
import accountGrantSchema from "../../schemas/schemas/logicsrc-account-grant.schema.json" with { type: "json" };
import accountProviderSchema from "../../schemas/schemas/logicsrc-account-provider.schema.json" with { type: "json" };
import connectedAccountSchema from "../../schemas/schemas/logicsrc-connected-account.schema.json" with { type: "json" };
import emailMessageSchema from "../../schemas/schemas/logicsrc-email-message.schema.json" with { type: "json" };
import eventSchema from "../../schemas/schemas/logicsrc-event.schema.json" with { type: "json" };
import pluginSchema from "../../schemas/schemas/logicsrc-plugin.schema.json" with { type: "json" };
import runSchema from "../../schemas/schemas/logicsrc-run.schema.json" with { type: "json" };
import socialPostSchema from "../../schemas/schemas/logicsrc-social-post.schema.json" with { type: "json" };
import taskSchema from "../../schemas/schemas/logicsrc-task.schema.json" with { type: "json" };
export const schemas = {
agent: agentSchema,
"account-audit-event": accountAuditEventSchema,
"account-grant": accountGrantSchema,
"account-provider": accountProviderSchema,
"connected-account": connectedAccountSchema,
"email-message": emailMessageSchema,
event: eventSchema,
plugin: pluginSchema,
run: runSchema,
"social-post": socialPostSchema,
task: taskSchema
} as const;