From c23ce42948349adf98750b341d81fd0eb4930788 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 9 Jun 2026 10:02:07 +0000 Subject: [PATCH] Add communication account plugin scaffolds --- apps/commandboard-api/package.json | 2 + apps/commandboard-api/src/contract.test.ts | 20 +- apps/commandboard-api/src/index.ts | 38 +- docs/cli.md | 16 + docs/communication-accounts.md | 139 ++++++ docs/data-model.md | 6 + docs/openapi.yaml | 24 + docs/permissions.md | 36 ++ docs/plugins.md | 3 + docs/roadmap.md | 5 +- package-lock.json | 48 ++ package.json | 2 +- packages/account-core/package.json | 15 + packages/account-core/src/audit.ts | 43 ++ packages/account-core/src/index.test.ts | 57 +++ packages/account-core/src/index.ts | 13 + packages/account-core/src/permissions.ts | 59 +++ packages/account-core/src/policy.ts | 92 ++++ .../account-core/src/provider-registry.ts | 28 ++ packages/account-core/src/types.ts | 447 ++++++++++++++++++ packages/account-core/tsconfig.json | 8 + packages/cli/package.json | 3 + packages/cli/src/index.ts | 129 +++++ packages/cli/src/registry.ts | 4 +- packages/logicsrc-mcp/src/server.ts | 72 ++- packages/schemas/package.json | 8 +- .../logicsrc-account-audit-event.schema.json | 31 ++ .../logicsrc-account-grant.schema.json | 48 ++ .../logicsrc-account-provider.schema.json | 31 ++ .../logicsrc-connected-account.schema.json | 39 ++ .../logicsrc-email-message.schema.json | 22 + .../schemas/logicsrc-social-post.schema.json | 26 + packages/tui/package.json | 2 + packages/tui/src/index.ts | 8 +- packages/validators/src/schemas.ts | 12 + plugins/email-accounts/README.md | 5 + plugins/email-accounts/package.json | 19 + plugins/email-accounts/src/index.ts | 43 ++ plugins/email-accounts/src/manifest.ts | 39 ++ plugins/email-accounts/src/providers/index.ts | 40 ++ plugins/email-accounts/tsconfig.json | 8 + plugins/social-accounts/README.md | 5 + plugins/social-accounts/package.json | 19 + plugins/social-accounts/src/index.ts | 41 ++ plugins/social-accounts/src/manifest.ts | 37 ++ .../social-accounts/src/providers/index.ts | 49 ++ plugins/social-accounts/tsconfig.json | 8 + .../20260609010000_communication_accounts.sql | 113 +++++ 48 files changed, 1949 insertions(+), 13 deletions(-) create mode 100644 docs/communication-accounts.md create mode 100644 packages/account-core/package.json create mode 100644 packages/account-core/src/audit.ts create mode 100644 packages/account-core/src/index.test.ts create mode 100644 packages/account-core/src/index.ts create mode 100644 packages/account-core/src/permissions.ts create mode 100644 packages/account-core/src/policy.ts create mode 100644 packages/account-core/src/provider-registry.ts create mode 100644 packages/account-core/src/types.ts create mode 100644 packages/account-core/tsconfig.json create mode 100644 packages/schemas/schemas/logicsrc-account-audit-event.schema.json create mode 100644 packages/schemas/schemas/logicsrc-account-grant.schema.json create mode 100644 packages/schemas/schemas/logicsrc-account-provider.schema.json create mode 100644 packages/schemas/schemas/logicsrc-connected-account.schema.json create mode 100644 packages/schemas/schemas/logicsrc-email-message.schema.json create mode 100644 packages/schemas/schemas/logicsrc-social-post.schema.json create mode 100644 plugins/email-accounts/README.md create mode 100644 plugins/email-accounts/package.json create mode 100644 plugins/email-accounts/src/index.ts create mode 100644 plugins/email-accounts/src/manifest.ts create mode 100644 plugins/email-accounts/src/providers/index.ts create mode 100644 plugins/email-accounts/tsconfig.json create mode 100644 plugins/social-accounts/README.md create mode 100644 plugins/social-accounts/package.json create mode 100644 plugins/social-accounts/src/index.ts create mode 100644 plugins/social-accounts/src/manifest.ts create mode 100644 plugins/social-accounts/src/providers/index.ts create mode 100644 plugins/social-accounts/tsconfig.json create mode 100644 supabase/migrations/20260609010000_communication_accounts.sql diff --git a/apps/commandboard-api/package.json b/apps/commandboard-api/package.json index bad9548..ef70c40 100644 --- a/apps/commandboard-api/package.json +++ b/apps/commandboard-api/package.json @@ -15,8 +15,10 @@ "@logicsrc/plugin-core": "file:../../packages/plugin-core", "@logicsrc/plugin-c0mpute": "file:../../plugins/c0mpute", "@logicsrc/plugin-coinpay": "file:../../plugins/coinpay", + "@logicsrc/plugin-email-accounts": "file:../../plugins/email-accounts", "@logicsrc/plugin-feed-discovery": "file:../../plugins/feed-discovery", "@logicsrc/plugin-sh1pt": "file:../../plugins/sh1pt", + "@logicsrc/plugin-social-accounts": "file:../../plugins/social-accounts", "@logicsrc/plugin-ugig": "file:../../plugins/ugig", "@logicsrc/validators": "file:../../packages/validators" }, diff --git a/apps/commandboard-api/src/contract.test.ts b/apps/commandboard-api/src/contract.test.ts index c6bc7b0..2b9629d 100644 --- a/apps/commandboard-api/src/contract.test.ts +++ b/apps/commandboard-api/src/contract.test.ts @@ -47,7 +47,7 @@ describe("CommandBoard API contracts", () => { }; expect(response.status).toBe(200); - expect(body.plugins.map((plugin) => plugin.id)).toEqual(["coinpay", "ugig", "sh1pt", "c0mpute", "feed-discovery"]); + expect(body.plugins.map((plugin) => plugin.id)).toEqual(["coinpay", "ugig", "sh1pt", "c0mpute", "feed-discovery", "social-accounts", "email-accounts"]); expect(body.plugins.find((plugin) => plugin.id === "sh1pt")).toMatchObject({ enabled: true, capabilities: expect.arrayContaining(["projects.sync", "actions.publish", "deployments.status"]) @@ -59,6 +59,24 @@ describe("CommandBoard API contracts", () => { expect(body.capabilities["actions.publish"]).toEqual(["sh1pt"]); expect(body.capabilities["compute.jobs.dispatch"]).toEqual(["c0mpute"]); expect(body.capabilities["feeds.discover"]).toEqual(["feed-discovery"]); + expect(body.capabilities["social.post.publish"]).toEqual(["social-accounts"]); + expect(body.capabilities["email.send"]).toEqual(["email-accounts"]); + }); + + it("exposes communication account provider contracts", async () => { + const accountsResponse = await fetch(`${baseUrl}/api/accounts/providers`); + const accountsBody = await accountsResponse.json() as { providers: Array<{ id: string; kind: string }> }; + const socialResponse = await fetch(`${baseUrl}/api/social/providers`); + const socialBody = await socialResponse.json() as { providers: Array<{ id: string; kind: string }> }; + const emailResponse = await fetch(`${baseUrl}/api/email/providers`); + const emailBody = await emailResponse.json() as { providers: Array<{ id: string; kind: string }> }; + + expect(accountsResponse.status).toBe(200); + expect(accountsBody.providers.map((provider) => provider.id)).toEqual(expect.arrayContaining(["mastodon", "gmail", "imap-smtp"])); + expect(socialResponse.status).toBe(200); + expect(socialBody.providers[0]).toMatchObject({ id: "mastodon", kind: "social" }); + expect(emailResponse.status).toBe(200); + expect(emailBody.providers[0]).toMatchObject({ id: "imap-smtp", kind: "email" }); }); it("exposes feed discovery plugin endpoints", async () => { diff --git a/apps/commandboard-api/src/index.ts b/apps/commandboard-api/src/index.ts index 41b2f43..58b6166 100644 --- a/apps/commandboard-api/src/index.ts +++ b/apps/commandboard-api/src/index.ts @@ -3,12 +3,14 @@ import { pathToFileURL } from "node:url"; import { createPluginRegistry } from "@logicsrc/plugin-core"; import { c0mputePlugin } from "@logicsrc/plugin-c0mpute"; import { coinPayPlugin } from "@logicsrc/plugin-coinpay"; +import { emailAccountsPlugin, listEmailAccountProviders } from "@logicsrc/plugin-email-accounts"; import { discoverFeeds, feedDiscoveryPlugin, listFeedProviders, renderAtom, renderJsonFeed, renderOpml, renderRss, type FeedKind } from "@logicsrc/plugin-feed-discovery"; import { sh1ptPlugin } from "@logicsrc/plugin-sh1pt"; +import { listSocialAccountProviders, socialAccountsPlugin } from "@logicsrc/plugin-social-accounts"; import { uGigPlugin } from "@logicsrc/plugin-ugig"; import { schemas, validate } from "@logicsrc/validators"; -const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, sh1ptPlugin, c0mputePlugin, feedDiscoveryPlugin]); +const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, sh1ptPlugin, c0mputePlugin, feedDiscoveryPlugin, socialAccountsPlugin, emailAccountsPlugin]); const boards = [ { path: "/general", title: "General", description: "CommandBoard.run general discussion." }, @@ -70,7 +72,7 @@ async function route(request: IncomingMessage, response: ServerResponse) { json(response, 200, { ok: true, service: "commandboard-api", - endpoints: ["/health", "/api/boards", "/api/tasks", "/api/plugins", "/api/schemas", "/api/feeds/discover", "/api/feeds/providers", "/rss/discover/:keyword.xml", "/opml/discover/:keyword.xml", "/atom/discover/:keyword.xml", "/json-feed/discover/:keyword.json"] + endpoints: ["/health", "/api/boards", "/api/tasks", "/api/plugins", "/api/schemas", "/api/accounts/providers", "/api/accounts", "/api/social/providers", "/api/email/providers", "/api/feeds/discover", "/api/feeds/providers", "/rss/discover/:keyword.xml", "/opml/discover/:keyword.xml", "/atom/discover/:keyword.xml", "/json-feed/discover/:keyword.json"] }); return; } @@ -114,6 +116,38 @@ async function route(request: IncomingMessage, response: ServerResponse) { return; } + if (request.method === "GET" && url.pathname === "/api/accounts/providers") { + const kind = url.searchParams.get("kind"); + const providers = [...listSocialAccountProviders(), ...listEmailAccountProviders()].filter((provider) => !kind || provider.kind === kind); + json(response, 200, { providers }); + return; + } + + if (request.method === "GET" && url.pathname === "/api/accounts") { + json(response, 200, { accounts: [] }); + return; + } + + if (request.method === "GET" && url.pathname === "/api/social/providers") { + json(response, 200, { providers: listSocialAccountProviders() }); + return; + } + + if (request.method === "GET" && url.pathname === "/api/social/accounts") { + json(response, 200, { accounts: [] }); + return; + } + + if (request.method === "GET" && url.pathname === "/api/email/providers") { + json(response, 200, { providers: listEmailAccountProviders() }); + return; + } + + if (request.method === "GET" && url.pathname === "/api/email/accounts") { + json(response, 200, { accounts: [] }); + return; + } + if (request.method === "GET" && (url.pathname === "/api/feeds/providers" || url.pathname === "/api/rss/providers")) { json(response, 200, { providers: listFeedProviders() }); return; diff --git a/docs/cli.md b/docs/cli.md index 3f05f97..46fbb12 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -31,6 +31,9 @@ task wallet events agentswarm +accounts +social +email credentials openspec plugins @@ -66,6 +69,19 @@ logicsrc credentials plan --from doppler --to github-secrets Credential sharing is provider-neutral. External tools can consume LogicSRC credential contracts, but LogicSRC commands do not call proprietary product CLIs. +Communication account commands: + +```bash +logicsrc accounts providers +logicsrc accounts list +logicsrc social providers +logicsrc email providers +logicsrc social post --text "Launching today" --dry-run +logicsrc email send --dry-run +``` + +Communication account commands must redact credentials, support dry-run for write-capable actions, and require approval policies for outbound email and social publishing. + When `--openspec` is enabled, AgentSwarm writes OpenSpec.dev-style files under `openspec/changes//`. Machine-readable output should be available anywhere data is returned: diff --git a/docs/communication-accounts.md b/docs/communication-accounts.md new file mode 100644 index 0000000..c2ccce1 --- /dev/null +++ b/docs/communication-accounts.md @@ -0,0 +1,139 @@ +# Communication Accounts + +Status: draft implementation scaffold + +Slug: `communication-accounts` + +Communication Accounts is the LogicSRC standard for connecting external social and email identities, delegating scoped access to humans, agents, workflows, and plugins, and auditing every read/write action without exposing raw credentials. + +## First-Party Plugins + +```txt +social-accounts +email-accounts +``` + +- `@logicsrc/plugin-social-accounts` manages social/network accounts such as Mastodon, Bluesky, GitHub, X/Twitter, Reddit, LinkedIn, YouTube, Discord, Telegram, and future ActivityPub/RSS-style adapters. +- `@logicsrc/plugin-email-accounts` manages email inboxes and outbound sending identities through IMAP/SMTP, Gmail, Microsoft Graph, ForwardEmail.net, local bridges, and custom providers. + +Both plugins share contracts from `@logicsrc/account-core`. + +## Core Objects + +```txt +connected_account +account_provider +account_permission_grant +account_policy +account_audit_event +credential_broker_call +email_message_cache +social_post_cache +``` + +The shared account model records account kind, provider, account display metadata, granted scopes, declared capabilities, status, credential reference, ownership scope, and sync timestamps. + +## Provider Boundary + +Provider adapters implement the shared account provider contract: + +```txt +provider.id +provider.kind +provider.authMethods +provider.capabilities +provider.getAuthUrl() +provider.completeAuth() +provider.refreshCredential() +provider.testConnection() +provider.revoke() +``` + +Social providers extend this with profile, draft, publish, media, mentions, comments, and analytics operations. + +Email providers extend this with search, read, draft, send, reply, forward, archive, label, and delete operations. + +## Permissions + +Shared permissions: + +```txt +accounts:connect +accounts:list +accounts:read_metadata +accounts:test +accounts:revoke +accounts:sync +accounts:audit:read +``` + +Social and email permissions use colon-style grant scopes such as `social:post:publish`, `social:mentions:read`, `email:headers:read`, `email:send`, and `email:attachments:read`. + +Plugin manifest capabilities use the existing LogicSRC dotted capability convention such as `social.post.publish` and `email.headers.read`. + +## Policy Gates + +These actions require policy evaluation by default: + +```txt +social:post:publish +social:post:delete +social:dm:read +social:dm:send +email:attachments:read +email:send +email:delete +``` + +Default write behavior is dry-run first, approval required for public publishing and outbound email, and deny for critical risk unless an admin override exists. + +## Credential Rules + +- Raw secrets must never be printed or exposed to agents by default. +- OAuth refresh tokens and app passwords must be stored through a credential broker. +- Provider calls should execute through a trusted runtime boundary. +- Credential access must create audit events. +- CLI, API, MCP, TUI, and PWA previews must redact secret-like fields. + +## CLI Namespaces + +```bash +logicsrc accounts providers +logicsrc accounts list +logicsrc accounts audit +logicsrc social providers +logicsrc social accounts +logicsrc social post --text "Launching today" --dry-run +logicsrc email providers +logicsrc email accounts +logicsrc email send --dry-run +``` + +The initial scaffold exposes provider listings and dry-run placeholders. Live connect, sync, send, and publish flows require durable credential broker, approval queue, and audit persistence. + +## Storage + +Deployable database migrations live under `supabase/migrations/`. + +The communication account scaffold adds: + +```txt +connected_accounts +account_permission_grants +account_audit_events +email_message_cache +social_post_cache +``` + +## MVP Order + +1. Stabilize `packages/account-core`. +2. Validate account schemas through `packages/validators`. +3. Apply Supabase account migrations. +4. Wire provider and account list commands. +5. Add API read/list/dry-run endpoints and contract tests. +6. Add MCP read-only resources. +7. Add TUI/PWA account status panels. +8. Implement IMAP/SMTP provider behind approval and audit gates. +9. Implement Mastodon, Bluesky, or GitHub social provider behind approval and audit gates. +10. Add Gmail OAuth after credential broker storage and restricted-scope handling are complete. diff --git a/docs/data-model.md b/docs/data-model.md index 22e06e4..4e3b7a3 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -6,6 +6,9 @@ Core tables: users dids oauth_accounts +connected_accounts +account_permission_grants +account_audit_events profiles organizations organization_members @@ -34,6 +37,8 @@ notifications schemas schema_versions plugin_audit_logs +email_message_cache +social_post_cache ``` Important relationships: @@ -50,3 +55,4 @@ Important relationships: - API keys belong to users, agents, or service accounts. - Permissions are scoped to resources. - LogicSRC-compatible objects record their schema version. +- Connected social and email accounts reference credentials through a broker and use account permission grants plus account audit events for agent-safe delegation. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0758c71..bb2d49d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -43,6 +43,30 @@ paths: responses: "201": description: Credential sync plan created + /api/accounts/providers: + get: + summary: List social and email account providers + responses: + "200": + description: Account providers returned + /api/accounts: + get: + summary: List connected social and email accounts + responses: + "200": + description: Connected accounts returned + /api/social/providers: + get: + summary: List social account providers + responses: + "200": + description: Social account providers returned + /api/email/providers: + get: + summary: List email account providers + responses: + "200": + description: Email account providers returned /api/schemas: get: summary: List supported LogicSRC schema kinds diff --git a/docs/permissions.md b/docs/permissions.md index e6c70d9..25d1c6d 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -40,3 +40,39 @@ payment:spend_limited ``` Spend controls must include per-run, per-day, and per-task limits. Agents must never receive wallet private keys. + +Communication account scopes: + +```txt +accounts:connect +accounts:list +accounts:read_metadata +accounts:test +accounts:revoke +accounts:sync +accounts:audit:read +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 +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 +``` + +Outbound email, social publishing, private-message access, attachment reads, and destructive actions require policy gates and audit records by default. diff --git a/docs/plugins.md b/docs/plugins.md index 8686e8c..a13744b 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -8,11 +8,14 @@ Default plugins: - uGig: job import, gig publishing, candidate/agent linking, bid sync, marketplace publishing, and reputation sync. - c0mpute: compute job dispatch, worker pool sync, usage reporting, quote creation, settlement status, and compute reputation events. - Credential Sharing: provider-neutral secret sync plans, approvals, rollbacks, and audit events. +- Social Accounts: provider-neutral social profile, drafting, publishing, sync, policy, and audit flows. +- Email Accounts: provider-neutral inbox, sending identity, search, draft, send, sync, policy, and audit flows. Coming soon plugin specs: - AgentByte: candidate, contractor, and agent capability screening for AI-era workflows. See `docs/agent-screening.md`. - Credential Sharing: replacement architecture for .env, Doppler, Railway variables, GitHub Secrets, and future providers. See `docs/credential-sharing.md`. +- Communication Accounts: shared social and email account management contracts. See `docs/communication-accounts.md`. Runtime requirements: diff --git a/docs/roadmap.md b/docs/roadmap.md index ce9ef5d..f672012 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -27,5 +27,6 @@ 25. Add SDK contracts for Rust, Bun, Node, Python, and curl. 26. Add MCP server contracts. 27. Add credential sync audit exports. -28. Add docs. -29. Tag v1.0.0. +28. Add communication account contracts, plugins, schemas, and account audit events. +29. Add docs. +30. Tag v1.0.0. diff --git a/package-lock.json b/package-lock.json index c36e273..6d2c8af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,10 @@ "@logicsrc/plugin-c0mpute": "file:../../plugins/c0mpute", "@logicsrc/plugin-coinpay": "file:../../plugins/coinpay", "@logicsrc/plugin-core": "file:../../packages/plugin-core", + "@logicsrc/plugin-email-accounts": "file:../../plugins/email-accounts", "@logicsrc/plugin-feed-discovery": "file:../../plugins/feed-discovery", "@logicsrc/plugin-sh1pt": "file:../../plugins/sh1pt", + "@logicsrc/plugin-social-accounts": "file:../../plugins/social-accounts", "@logicsrc/plugin-ugig": "file:../../plugins/ugig", "@logicsrc/validators": "file:../../packages/validators" }, @@ -1424,6 +1426,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@logicsrc/account-core": { + "resolved": "packages/account-core", + "link": true + }, "node_modules/@logicsrc/cli": { "resolved": "packages/cli", "link": true @@ -1448,6 +1454,10 @@ "resolved": "packages/plugin-core", "link": true }, + "node_modules/@logicsrc/plugin-email-accounts": { + "resolved": "plugins/email-accounts", + "link": true + }, "node_modules/@logicsrc/plugin-feed-discovery": { "resolved": "plugins/feed-discovery", "link": true @@ -1456,6 +1466,10 @@ "resolved": "plugins/sh1pt", "link": true }, + "node_modules/@logicsrc/plugin-social-accounts": { + "resolved": "plugins/social-accounts", + "link": true + }, "node_modules/@logicsrc/plugin-ugig": { "resolved": "plugins/ugig", "link": true @@ -6042,13 +6056,23 @@ "zod": "^3.25.28 || ^4" } }, + "packages/account-core": { + "name": "@logicsrc/account-core", + "version": "0.1.0", + "devDependencies": { + "vitest": "^4.0.8" + } + }, "packages/cli": { "name": "@logicsrc/cli", "version": "0.1.0", "dependencies": { + "@logicsrc/account-core": "file:../account-core", "@logicsrc/plugin-coinpay": "file:../../plugins/coinpay", "@logicsrc/plugin-core": "file:../plugin-core", + "@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", @@ -6104,6 +6128,8 @@ "dependencies": { "@logicsrc/plugin-coinpay": "file:../../plugins/coinpay", "@logicsrc/plugin-core": "file:../plugin-core", + "@logicsrc/plugin-email-accounts": "file:../../plugins/email-accounts", + "@logicsrc/plugin-social-accounts": "file:../../plugins/social-accounts", "@logicsrc/plugin-ugig": "file:../../plugins/ugig" } }, @@ -6142,6 +6168,17 @@ "vitest": "^4.0.8" } }, + "plugins/email-accounts": { + "name": "@logicsrc/plugin-email-accounts", + "version": "0.1.0", + "dependencies": { + "@logicsrc/account-core": "file:../../packages/account-core", + "@logicsrc/plugin-core": "file:../../packages/plugin-core" + }, + "devDependencies": { + "vitest": "^4.0.8" + } + }, "plugins/feed-discovery": { "name": "@logicsrc/plugin-feed-discovery", "version": "0.1.0", @@ -6168,6 +6205,17 @@ "vitest": "^4.0.8" } }, + "plugins/social-accounts": { + "name": "@logicsrc/plugin-social-accounts", + "version": "0.1.0", + "dependencies": { + "@logicsrc/account-core": "file:../../packages/account-core", + "@logicsrc/plugin-core": "file:../../packages/plugin-core" + }, + "devDependencies": { + "vitest": "^4.0.8" + } + }, "plugins/ugig": { "name": "@logicsrc/plugin-ugig", "version": "0.1.0", diff --git a/package.json b/package.json index 9a9cd93..c0deda1 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "apps/*" ], "scripts": { - "build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/plugin-c0mpute run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build", + "build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/account-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/plugin-c0mpute run build && npm --workspace @logicsrc/plugin-feed-discovery run build && npm --workspace @logicsrc/plugin-social-accounts run build && npm --workspace @logicsrc/plugin-email-accounts run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build", "start": "npm --workspace @logicsrc/web run start", "test": "npm run test --workspaces --if-present", "check": "npm run build && npm run test", diff --git a/packages/account-core/package.json b/packages/account-core/package.json new file mode 100644 index 0000000..4e682a5 --- /dev/null +++ b/packages/account-core/package.json @@ -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" + } +} diff --git a/packages/account-core/src/audit.ts b/packages/account-core/src/audit.ts new file mode 100644 index 0000000..c6bb5fd --- /dev/null +++ b/packages/account-core/src/audit.ts @@ -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) { + const output: Record = {}; + + 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; + resultPreview?: Record; + 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() + }; +} diff --git a/packages/account-core/src/index.test.ts b/packages/account-core/src/index.test.ts new file mode 100644 index 0000000..c00a8e3 --- /dev/null +++ b/packages/account-core/src/index.test.ts @@ -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"); + }); +}); diff --git a/packages/account-core/src/index.ts b/packages/account-core/src/index.ts new file mode 100644 index 0000000..3df1633 --- /dev/null +++ b/packages/account-core/src/index.ts @@ -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"; diff --git a/packages/account-core/src/permissions.ts b/packages/account-core/src/permissions.ts new file mode 100644 index 0000000..e674eea --- /dev/null +++ b/packages/account-core/src/permissions.ts @@ -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); +} diff --git a/packages/account-core/src/policy.ts b/packages/account-core/src/policy.ts new file mode 100644 index 0000000..b80ed25 --- /dev/null +++ b/packages/account-core/src/policy.ts @@ -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" }; +} diff --git a/packages/account-core/src/provider-registry.ts b/packages/account-core/src/provider-registry.ts new file mode 100644 index 0000000..42f1147 --- /dev/null +++ b/packages/account-core/src/provider-registry.ts @@ -0,0 +1,28 @@ +import type { LogicSrcAccountKind, LogicSrcAccountProviderManifest } from "./types.js"; + +export function createProviderRegistry(providers: LogicSrcAccountProviderManifest[]) { + const byId = new Map(); + + 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; + } + }; +} diff --git a/packages/account-core/src/types.ts b/packages/account-core/src/types.ts new file mode 100644 index 0000000..6ad5511 --- /dev/null +++ b/packages/account-core/src/types.ts @@ -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; + 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; + completeAuth?(input: CompleteAuthInput): Promise; + refreshCredential?(input: RefreshCredentialInput): Promise; + testConnection(input: TestConnectionInput): Promise; + revoke?(input: RevokeAccountInput): Promise; +} + +export interface SocialAccountProvider extends LogicSrcAccountProvider { + kind: "social"; + getProfile(input: SocialAccountInput): Promise; + draftPost(input: DraftSocialPostInput): Promise; + publishPost(input: PublishSocialPostInput): Promise; + uploadMedia?(input: UploadSocialMediaInput): Promise; + searchMentions?(input: SearchMentionsInput): Promise; + listComments?(input: ListCommentsInput): Promise; + getAnalytics?(input: SocialAnalyticsInput): Promise; +} + +export interface EmailAccountProvider extends LogicSrcAccountProvider { + kind: "email"; + searchMessages(input: EmailSearchInput): Promise; + readMessage(input: ReadEmailMessageInput): Promise; + draftMessage(input: DraftEmailInput): Promise; + sendMessage(input: SendEmailInput): Promise; + replyToMessage?(input: ReplyEmailInput): Promise; + forwardMessage?(input: ForwardEmailInput): Promise; + archiveMessage?(input: EmailMessageMutationInput): Promise; + applyLabels?(input: EmailLabelInput): Promise; + deleteMessage?(input: EmailMessageMutationInput): Promise; +} + +export interface AuthUrlInput { + redirectUri: string; + state: string; + scopes: string[]; + metadata?: Record; +} + +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; +} + +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; + storeCredential(input: CredentialBrokerStoreInput): Promise; + revokeCredential(input: CredentialBrokerRevokeInput): Promise; +} + +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; +} + +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; + resultPreview: Record; + 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; +} + +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; + metadata?: Record; +} + +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; +} diff --git a/packages/account-core/tsconfig.json b/packages/account-core/tsconfig.json new file mode 100644 index 0000000..df59da5 --- /dev/null +++ b/packages/account-core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index d75eb68..eaddef5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index acfa839..fb169d6 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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 ", "Source provider", "env" ); }); +const accounts = program.command("accounts").description("Manage connected social and email accounts."); + +accounts + .command("providers") + .option("--kind ", "social or email") + .option("--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 ", "social or email") + .option("--format ", "table, json, or markdown", "table") + .description("List connected accounts.") + .action((options) => { + print([], options.format as OutputFormat); + }); + +accounts + .command("audit") + .argument("", "Connected account id") + .option("--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 ", "table, json, or markdown", "table").description("List social account providers.").action((options) => { + print(listSocialAccountProviders(), options.format as OutputFormat); +}); + +social.command("accounts").option("--format ", "table, json, or markdown", "table").description("List connected social accounts.").action((options) => { + print([], options.format as OutputFormat); +}); + +social + .command("post") + .argument("", "Connected social account id") + .requiredOption("--text ", "Post text") + .option("--dry-run", "Evaluate without publishing", false) + .option("--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 ", "table, json, or markdown", "table").description("List email account providers.").action((options) => { + print(listEmailAccountProviders(), options.format as OutputFormat); +}); + +email.command("accounts").option("--format ", "table, json, or markdown", "table").description("List connected email accounts.").action((options) => { + print([], options.format as OutputFormat); +}); + +email + .command("send") + .argument("", "Email draft id") + .option("--dry-run", "Evaluate without sending", false) + .option("--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()); diff --git a/packages/cli/src/registry.ts b/packages/cli/src/registry.ts index 6081ee8..090a5f0 100644 --- a/packages/cli/src/registry.ts +++ b/packages/cli/src/registry.ts @@ -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]); } diff --git a/packages/logicsrc-mcp/src/server.ts b/packages/logicsrc-mcp/src/server.ts index 7247c90..46161df 100644 --- a/packages/logicsrc-mcp/src/server.ts +++ b/packages/logicsrc-mcp/src/server.ts @@ -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", diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 49a0b3d..8cde0d2 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -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" diff --git a/packages/schemas/schemas/logicsrc-account-audit-event.schema.json b/packages/schemas/schemas/logicsrc-account-audit-event.schema.json new file mode 100644 index 0000000..e409c7c --- /dev/null +++ b/packages/schemas/schemas/logicsrc-account-audit-event.schema.json @@ -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" } + } +} diff --git a/packages/schemas/schemas/logicsrc-account-grant.schema.json b/packages/schemas/schemas/logicsrc-account-grant.schema.json new file mode 100644 index 0000000..8620853 --- /dev/null +++ b/packages/schemas/schemas/logicsrc-account-grant.schema.json @@ -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" } + } +} diff --git a/packages/schemas/schemas/logicsrc-account-provider.schema.json b/packages/schemas/schemas/logicsrc-account-provider.schema.json new file mode 100644 index 0000000..bd6f2e1 --- /dev/null +++ b/packages/schemas/schemas/logicsrc-account-provider.schema.json @@ -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" } + } +} diff --git a/packages/schemas/schemas/logicsrc-connected-account.schema.json b/packages/schemas/schemas/logicsrc-connected-account.schema.json new file mode 100644 index 0000000..e78c497 --- /dev/null +++ b/packages/schemas/schemas/logicsrc-connected-account.schema.json @@ -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" } + } +} diff --git a/packages/schemas/schemas/logicsrc-email-message.schema.json b/packages/schemas/schemas/logicsrc-email-message.schema.json new file mode 100644 index 0000000..9d75743 --- /dev/null +++ b/packages/schemas/schemas/logicsrc-email-message.schema.json @@ -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" } + } +} diff --git a/packages/schemas/schemas/logicsrc-social-post.schema.json b/packages/schemas/schemas/logicsrc-social-post.schema.json new file mode 100644 index 0000000..7e91376 --- /dev/null +++ b/packages/schemas/schemas/logicsrc-social-post.schema.json @@ -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" } + } +} diff --git a/packages/tui/package.json b/packages/tui/package.json index 9c8e73d..e017116 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -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" } } diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 4540f7e..9a89965 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -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 = {}) { 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 = {}) { "│ /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(", ")}`) diff --git a/packages/validators/src/schemas.ts b/packages/validators/src/schemas.ts index f016936..b8f026c 100644 --- a/packages/validators/src/schemas.ts +++ b/packages/validators/src/schemas.ts @@ -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; diff --git a/plugins/email-accounts/README.md b/plugins/email-accounts/README.md new file mode 100644 index 0000000..171eb1f --- /dev/null +++ b/plugins/email-accounts/README.md @@ -0,0 +1,5 @@ +# LogicSRC Email Accounts Plugin + +First-party LogicSRC plugin for connecting and governing email inboxes and outbound sending identities. + +Initial provider manifests are intentionally contract-only. Live IMAP/SMTP, Gmail, and Microsoft Graph providers should be added behind `EmailAccountProvider` from `@logicsrc/account-core` after credential broker, approval, and audit persistence are available. diff --git a/plugins/email-accounts/package.json b/plugins/email-accounts/package.json new file mode 100644 index 0000000..0a50639 --- /dev/null +++ b/plugins/email-accounts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@logicsrc/plugin-email-accounts", + "version": "0.1.0", + "description": "LogicSRC email account management plugin for inbox, sending identity, provider, policy, and audit flows.", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run src --passWithNoTests" + }, + "dependencies": { + "@logicsrc/account-core": "file:../../packages/account-core", + "@logicsrc/plugin-core": "file:../../packages/plugin-core" + }, + "devDependencies": { + "vitest": "^4.0.8" + } +} diff --git a/plugins/email-accounts/src/index.ts b/plugins/email-accounts/src/index.ts new file mode 100644 index 0000000..7463f14 --- /dev/null +++ b/plugins/email-accounts/src/index.ts @@ -0,0 +1,43 @@ +import { createProviderRegistry } from "@logicsrc/account-core"; +import type { PluginDefinition } from "@logicsrc/plugin-core"; +import { emailAccountsManifest } from "./manifest.js"; +import { emailAccountProviderManifests } from "./providers/index.js"; + +export const emailAccountsPlugin: PluginDefinition = { + manifest: emailAccountsManifest, + configDefaults: { + enabled: true, + default_send_policy: "approval_required", + credential_broker: "${LOGICSRC_CREDENTIAL_BROKER}" + }, + routes: [ + { method: "GET", path: "/api/email/providers", capability: "email.providers.list" }, + { method: "GET", path: "/api/email/accounts", capability: "accounts.list" }, + { method: "POST", path: "/api/email/accounts/:id/search", capability: "email.search" }, + { method: "GET", path: "/api/email/messages/:messageId", capability: "email.headers.read" }, + { method: "POST", path: "/api/email/accounts/:id/drafts", capability: "email.draft" }, + { method: "POST", path: "/api/email/drafts/:draftId/send", capability: "email.send" }, + { method: "POST", path: "/api/email/messages/:messageId/labels", capability: "email.labels.modify" }, + { method: "DELETE", path: "/api/email/messages/:messageId", capability: "email.delete" } + ], + permissions: [ + "accounts:list", + "accounts:read_metadata", + "accounts:audit:read", + "email:headers:read", + "email:search", + "email:draft", + "email:send", + "email:labels:modify", + "email:delete" + ], + tuiPanels: [{ id: "email-accounts", title: "Email Accounts" }] +}; + +const registry = createProviderRegistry(emailAccountProviderManifests); + +export function listEmailAccountProviders() { + return registry.list("email"); +} + +export { emailAccountsManifest, emailAccountProviderManifests }; diff --git a/plugins/email-accounts/src/manifest.ts b/plugins/email-accounts/src/manifest.ts new file mode 100644 index 0000000..b1c343a --- /dev/null +++ b/plugins/email-accounts/src/manifest.ts @@ -0,0 +1,39 @@ +import type { PluginManifest } from "@logicsrc/plugin-core"; + +export const emailAccountsManifest: PluginManifest = { + id: "email-accounts", + name: "Email Accounts", + version: "0.1.0", + type: ["communication", "accounts", "email"], + default: true, + capabilities: [ + "accounts.connect", + "accounts.list", + "accounts.read_metadata", + "accounts.test", + "accounts.revoke", + "accounts.sync", + "accounts.audit.read", + "email.providers.list", + "email.headers.read", + "email.body.read", + "email.attachments.read", + "email.search", + "email.draft", + "email.send", + "email.labels.modify", + "email.delete", + "email.sync" + ], + commands: ["accounts", "email"], + env: [ + "GMAIL_CLIENT_ID", + "GMAIL_CLIENT_SECRET", + "MICROSOFT_CLIENT_ID", + "MICROSOFT_CLIENT_SECRET", + "IMAP_HOST", + "IMAP_PORT", + "SMTP_HOST", + "SMTP_PORT" + ] +}; diff --git a/plugins/email-accounts/src/providers/index.ts b/plugins/email-accounts/src/providers/index.ts new file mode 100644 index 0000000..f0fb914 --- /dev/null +++ b/plugins/email-accounts/src/providers/index.ts @@ -0,0 +1,40 @@ +import type { LogicSrcAccountProviderManifest } from "@logicsrc/account-core"; + +export const emailAccountProviderManifests: LogicSrcAccountProviderManifest[] = [ + { + id: "imap-smtp", + name: "IMAP + SMTP", + kind: "email", + authMethods: ["imap_smtp"], + capabilities: ["email.headers.read", "email.search", "email.draft", "email.send"], + defaultScopes: ["imap.read", "smtp.send"], + status: "planned" + }, + { + id: "gmail", + name: "Gmail", + kind: "email", + authMethods: ["oauth2"], + capabilities: ["email.headers.read", "email.search", "email.body.read", "email.draft", "email.send", "email.labels.modify"], + defaultScopes: ["https://www.googleapis.com/auth/gmail.metadata"], + status: "planned" + }, + { + id: "microsoft-graph", + name: "Microsoft Graph / Outlook", + kind: "email", + authMethods: ["oauth2"], + capabilities: ["email.headers.read", "email.search", "email.body.read", "email.draft", "email.send"], + defaultScopes: ["Mail.ReadBasic", "Mail.Send", "offline_access"], + status: "planned" + }, + { + id: "forwardemail", + name: "ForwardEmail.net", + kind: "email", + authMethods: ["imap_smtp", "api_key"], + capabilities: ["email.headers.read", "email.search", "email.draft", "email.send"], + defaultScopes: ["imap.read", "smtp.send"], + status: "planned" + } +]; diff --git a/plugins/email-accounts/tsconfig.json b/plugins/email-accounts/tsconfig.json new file mode 100644 index 0000000..df59da5 --- /dev/null +++ b/plugins/email-accounts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/plugins/social-accounts/README.md b/plugins/social-accounts/README.md new file mode 100644 index 0000000..b06f67f --- /dev/null +++ b/plugins/social-accounts/README.md @@ -0,0 +1,5 @@ +# LogicSRC Social Accounts Plugin + +First-party LogicSRC plugin for connecting and governing social/network accounts. + +Initial provider manifests are intentionally contract-only. Live provider adapters should be added behind `SocialAccountProvider` from `@logicsrc/account-core` after credential broker, approval, and audit persistence are available. diff --git a/plugins/social-accounts/package.json b/plugins/social-accounts/package.json new file mode 100644 index 0000000..98bb213 --- /dev/null +++ b/plugins/social-accounts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@logicsrc/plugin-social-accounts", + "version": "0.1.0", + "description": "LogicSRC social account management plugin for provider-agnostic profile, drafting, publishing, sync, policy, and audit flows.", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run src --passWithNoTests" + }, + "dependencies": { + "@logicsrc/account-core": "file:../../packages/account-core", + "@logicsrc/plugin-core": "file:../../packages/plugin-core" + }, + "devDependencies": { + "vitest": "^4.0.8" + } +} diff --git a/plugins/social-accounts/src/index.ts b/plugins/social-accounts/src/index.ts new file mode 100644 index 0000000..5e9bcd9 --- /dev/null +++ b/plugins/social-accounts/src/index.ts @@ -0,0 +1,41 @@ +import { createProviderRegistry } from "@logicsrc/account-core"; +import type { PluginDefinition } from "@logicsrc/plugin-core"; +import { socialAccountsManifest } from "./manifest.js"; +import { socialAccountProviderManifests } from "./providers/index.js"; + +export const socialAccountsPlugin: PluginDefinition = { + manifest: socialAccountsManifest, + configDefaults: { + enabled: true, + default_publish_policy: "approval_required", + credential_broker: "${LOGICSRC_CREDENTIAL_BROKER}" + }, + routes: [ + { method: "GET", path: "/api/social/providers", capability: "social.providers.list" }, + { method: "GET", path: "/api/social/accounts", capability: "accounts.list" }, + { method: "GET", path: "/api/social/accounts/:id/profile", capability: "social.profile.read" }, + { method: "POST", path: "/api/social/accounts/:id/drafts", capability: "social.post.draft" }, + { method: "POST", path: "/api/social/accounts/:id/posts", capability: "social.post.publish" }, + { method: "GET", path: "/api/social/accounts/:id/mentions", capability: "social.mentions.read" }, + { method: "GET", path: "/api/social/accounts/:id/analytics", capability: "social.analytics.read" } + ], + permissions: [ + "accounts:list", + "accounts:read_metadata", + "accounts:audit:read", + "social:profile:read", + "social:post:draft", + "social:post:publish", + "social:mentions:read", + "social:analytics:read" + ], + tuiPanels: [{ id: "social-accounts", title: "Social Accounts" }] +}; + +const registry = createProviderRegistry(socialAccountProviderManifests); + +export function listSocialAccountProviders() { + return registry.list("social"); +} + +export { socialAccountsManifest, socialAccountProviderManifests }; diff --git a/plugins/social-accounts/src/manifest.ts b/plugins/social-accounts/src/manifest.ts new file mode 100644 index 0000000..8621743 --- /dev/null +++ b/plugins/social-accounts/src/manifest.ts @@ -0,0 +1,37 @@ +import type { PluginManifest } from "@logicsrc/plugin-core"; + +export const socialAccountsManifest: PluginManifest = { + id: "social-accounts", + name: "Social Accounts", + version: "0.1.0", + type: ["communication", "accounts", "social"], + default: true, + capabilities: [ + "accounts.connect", + "accounts.list", + "accounts.read_metadata", + "accounts.test", + "accounts.revoke", + "accounts.sync", + "accounts.audit.read", + "social.providers.list", + "social.profile.read", + "social.post.draft", + "social.post.publish", + "social.media.upload", + "social.mentions.read", + "social.analytics.read" + ], + commands: ["accounts", "social"], + env: [ + "MASTODON_CLIENT_ID", + "MASTODON_CLIENT_SECRET", + "BLUESKY_APP_PASSWORD", + "GITHUB_CLIENT_ID", + "GITHUB_CLIENT_SECRET", + "X_CLIENT_ID", + "X_CLIENT_SECRET", + "REDDIT_CLIENT_ID", + "REDDIT_CLIENT_SECRET" + ] +}; diff --git a/plugins/social-accounts/src/providers/index.ts b/plugins/social-accounts/src/providers/index.ts new file mode 100644 index 0000000..42af318 --- /dev/null +++ b/plugins/social-accounts/src/providers/index.ts @@ -0,0 +1,49 @@ +import type { LogicSrcAccountProviderManifest } from "@logicsrc/account-core"; + +export const socialAccountProviderManifests: LogicSrcAccountProviderManifest[] = [ + { + id: "mastodon", + name: "Mastodon", + kind: "social", + authMethods: ["oauth2"], + capabilities: ["social.profile.read", "social.post.draft", "social.post.publish", "social.mentions.read"], + defaultScopes: ["read:accounts", "read:statuses", "write:statuses"], + status: "planned" + }, + { + id: "bluesky", + name: "Bluesky", + kind: "social", + authMethods: ["api_key"], + capabilities: ["social.profile.read", "social.post.draft", "social.post.publish"], + defaultScopes: ["atproto.session", "atproto.repo.read", "atproto.repo.write"], + status: "planned" + }, + { + id: "github", + name: "GitHub", + kind: "social", + authMethods: ["oauth2", "api_key"], + capabilities: ["social.profile.read", "social.post.draft", "social.post.publish"], + defaultScopes: ["read:user", "repo"], + status: "planned" + }, + { + id: "x", + name: "X / Twitter", + kind: "social", + authMethods: ["oauth2", "api_key"], + capabilities: ["social.profile.read", "social.post.draft", "social.post.publish", "social.mentions.read", "social.analytics.read"], + defaultScopes: ["tweet.read", "tweet.write", "users.read", "offline.access"], + status: "planned" + }, + { + id: "reddit", + name: "Reddit", + kind: "social", + authMethods: ["oauth2"], + capabilities: ["social.profile.read", "social.post.draft", "social.post.publish", "social.comments.read"], + defaultScopes: ["identity", "read", "submit"], + status: "planned" + } +]; diff --git a/plugins/social-accounts/tsconfig.json b/plugins/social-accounts/tsconfig.json new file mode 100644 index 0000000..df59da5 --- /dev/null +++ b/plugins/social-accounts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/supabase/migrations/20260609010000_communication_accounts.sql b/supabase/migrations/20260609010000_communication_accounts.sql new file mode 100644 index 0000000..ff8dd1e --- /dev/null +++ b/supabase/migrations/20260609010000_communication_accounts.sql @@ -0,0 +1,113 @@ +create table if not exists connected_accounts ( + id uuid primary key default gen_random_uuid(), + org_id uuid, + project_id uuid, + board_id uuid, + owner_user_id uuid not null, + kind text not null check (kind in ('social', 'email')), + provider text not null, + provider_account_id text, + display_name text not null, + handle text, + email text, + avatar_url text, + homepage_url text, + status text not null default 'pending' check (status in ('connected', 'expired', 'revoked', 'error', 'disabled', 'pending')), + scopes text[] not null default '{}', + capabilities text[] not null default '{}', + credential_ref text not null, + metadata jsonb not null default '{}', + last_synced_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create unique index if not exists connected_accounts_provider_identity_idx +on connected_accounts(provider, provider_account_id) +where provider_account_id is not null; + +create index if not exists connected_accounts_owner_kind_idx +on connected_accounts(owner_user_id, kind); + +create table if not exists account_permission_grants ( + id uuid primary key default gen_random_uuid(), + account_id uuid not null references connected_accounts(id) on delete cascade, + principal_type text not null check (principal_type in ('user', 'agent', 'workflow', 'plugin')), + principal_id text not null, + permissions text[] not null default '{}', + policy jsonb not null default '[]', + expires_at timestamptz, + created_by uuid, + created_at timestamptz not null default now(), + revoked_at timestamptz +); + +create index if not exists account_permission_grants_principal_idx +on account_permission_grants(principal_type, principal_id) +where revoked_at is null; + +create table if not exists account_audit_events ( + id uuid primary key default gen_random_uuid(), + account_id uuid references connected_accounts(id) on delete set null, + provider text not null, + kind text not null check (kind in ('social', 'email')), + principal_type text not null check (principal_type in ('user', 'agent', 'workflow', 'plugin')), + principal_id text not null, + action text not null, + decision text not null check (decision in ('allow', 'approval_required', 'deny')), + risk_score numeric not null default 0 check (risk_score >= 0 and risk_score <= 1), + request_preview jsonb not null default '{}', + result_preview jsonb not null default '{}', + correlation_id text, + ip_address inet, + user_agent text, + created_at timestamptz not null default now() +); + +create index if not exists account_audit_events_account_created_idx +on account_audit_events(account_id, created_at desc); + +create index if not exists account_audit_events_principal_created_idx +on account_audit_events(principal_type, principal_id, created_at desc); + +create table if not exists email_message_cache ( + id uuid primary key default gen_random_uuid(), + account_id uuid not null references connected_accounts(id) on delete cascade, + provider_message_id text not null, + thread_id text, + subject text, + from_address text, + to_addresses text[] not null default '{}', + cc_addresses text[] not null default '{}', + snippet text, + labels text[] not null default '{}', + has_attachments boolean not null default false, + received_at timestamptz, + sent_at timestamptz, + metadata jsonb not null default '{}', + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + unique(account_id, provider_message_id) +); + +create index if not exists email_message_cache_account_received_idx +on email_message_cache(account_id, received_at desc); + +create table if not exists social_post_cache ( + id uuid primary key default gen_random_uuid(), + account_id uuid not null references connected_accounts(id) on delete cascade, + provider_post_id text, + status text not null default 'draft' check (status in ('draft', 'pending_approval', 'published', 'deleted', 'failed')), + text text, + media jsonb not null default '[]', + url text, + published_at timestamptz, + created_by_principal_type text check (created_by_principal_type in ('user', 'agent', 'workflow', 'plugin')), + created_by_principal_id text, + metadata jsonb not null default '{}', + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists social_post_cache_account_created_idx +on social_post_cache(account_id, created_at desc);