diff --git a/AGENTS.md b/AGENTS.md index 2a6bfbd..fd64a3f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ ChittyCommand is a unified life management and action dashboard for the ChittyOS **Repo:** `CHITTYOS/chittycommand` **Deploy:** Cloudflare Workers at `command.chitty.cc` **Stack:** Hono TypeScript, React + Tailwind, Neon PostgreSQL (via Hyperdrive), Cloudflare R2/KV -**Canonical URI:** `chittycanon://core/services/chittycommand` | Tier 5 +**Canonical URI:** `chittycanon://core/services/chittycommand` | Tier 2 (Platform) with Tier-5 dashboard surface ## Common Commands @@ -29,7 +29,7 @@ wrangler secret put DATABASE_URL ## Architecture -Single Cloudflare Worker (`chittycommand`) serving API + cron. Frontend is a separate React SPA at `app.command.chitty.cc` (Cloudflare Pages). +Cloudflare Worker (`chittycommand`) serves the API, ActionAgent, MCP, and request-side meta-orchestrator surfaces; a supervised persistent daemon consumes the same Goal → Plan → Intent and executor registry for durable dispatch. Frontend is a separate React SPA at `app.command.chitty.cc` (Cloudflare Pages). ### Data Sources @@ -76,7 +76,9 @@ Three modes: - `src/lib/validators.ts` — Zod schemas for request validation - `src/routes/bridge.ts` — Inter-service bridge (scrape, ledger, finance, Plaid) - `src/routes/mcp.ts` — MCP server for Codex integration (28 tools) -- `src/routes/meta.ts` — Public canon/schema/beacon + authenticated whoami +- `src/routes/meta.ts` — Public canon/schema/beacon + authenticated whoami/intent execution +- `meta/intent.ts` — Goal → Plan → Intent lifecycle +- `meta/executors/` — Canonical executor registry; includes read-only `legal_matter_bootstrap` projection to ChittyCounsel - `src/routes/connect.ts` — ChittyConnect discovery proxy (rate-limited) - `src/routes/ledger.ts` — ChittyLedger evidence/custody passthrough - `src/routes/context.ts` — Persona/context management (user + global) diff --git a/CHARTER.md b/CHARTER.md index 01cd9f5..0cf7757 100644 --- a/CHARTER.md +++ b/CHARTER.md @@ -88,6 +88,7 @@ Provide a unified life management and action dashboard that ingests data from 15 | Upstream | ChittyScrape | Browser-based scraping for portals without APIs | | Upstream | ChittyLedger | Evidence and document ledger sync | | Upstream | ChittyEvidence | Evidence facts, documents, entities for case timelines | +| Upstream | ChittyCounsel | Read-only legal matter bootstrap, case-strategy retrieval, and governed counsel work-product projection | | Upstream | ChittyConnect | Inter-service connectivity and discovery; **forever-context** read/write via ContextConsciousness + MemoryCloude; **sensitive-intent secret brokerage** path for all credential access (no inline secrets) | | Upstream | ChittyRouter | Unified ingestion gateway (scrape, email routing) | | Upstream | ChittySchema | Canonical schema validation and drift detection | @@ -155,6 +156,7 @@ Executors self-register at module load (side-effect imports from `meta/executors | `intent_type` | Canonical URI | Notes | |---------------|---------------|-------| | `update_obligation_status` | `chittycanon://core/services/chittycommand/executors/update_obligation_status` | Real Neon write to `cc_obligations`. Source: `meta/executors/update-obligation-status.ts` | +| `legal_matter_bootstrap` | `chittycanon://core/services/chittycommand/executors/legal_matter_bootstrap` | Read-only ChittyCounsel bootstrap projection. Requires `privileged` + `legalink`; writes no evidence or facts. Source: `meta/executors/legal-matter-bootstrap.ts` | > **Future executors** (e.g. `mercury_payment` — 🔒 REAL-MONEY, will require fresh `autonomous` sovereignty assessment and an enforced USD 500 per-intent cap) are tracked in ADR-001 but are NOT yet registered in `meta/executors/index.ts`. They will be added in follow-up PRs and listed here at the same time the executor file is committed. This table is the authoritative list of currently-registered executors — do not document executors that do not exist. diff --git a/CHITTY.md b/CHITTY.md index e2e87e7..0fb6dcd 100644 --- a/CHITTY.md +++ b/CHITTY.md @@ -87,8 +87,8 @@ Source: `chittycanon://gov/governance#three-aspects` ### Ecosystem Position -- **Upstream**: ChittyAuth, ChittyTrust, ChittyID, ChittyConnect (ContextConsciousness + MemoryCloude + sensitive-intent secret brokerage), chittyagent-orchestrator (routing + channel fanout), chittyagent-tasks (queue + lease pattern), chittyagent-ch1tty (MCP portal/OAuth), ChittyFinance, ChittyBooks, ChittyAssets, ChittyCharge, ChittyScrape, ChittyLedger, ChittyEvidence, ChittyRouter, ChittySchema, ChittyCert, ChittyRegister, ChittyChat. -- **Downstream / sub-channels**: ChittyServ cluster nodes (`chittymini-01..06`, `chittyserv-vm`) — each runs a supervised `daemon/` process and registers as an L-type sub-channel via `agent.chitty.cc/api/v1/channels`. ActionAgent (chat surface) and dashboard SPA at `app.command.chitty.cc` are sibling consumers of the same `meta/executors/*` registry. +- **Upstream**: ChittyAuth, ChittyTrust, ChittyID, ChittyConnect (ContextConsciousness + MemoryCloude + sensitive-intent secret brokerage), chittyagent-orchestrator (routing + channel fanout), chittyagent-tasks (queue + lease pattern), chittyagent-ch1tty (MCP portal/OAuth), ChittyFinance, ChittyBooks, ChittyAssets, ChittyCharge, ChittyScrape, ChittyLedger, ChittyEvidence, ChittyCounsel, ChittyRouter, ChittySchema, ChittyCert, ChittyRegister, ChittyChat. +- **Downstream / sub-channels**: ChittyServ cluster nodes (`chittymini-01..06`, `chittyserv-vm`) — each runs a supervised `daemon/` process and registers as an L-type sub-channel via `agent.chitty.cc/api/v1/channels`. ActionAgent (chat surface) and dashboard SPA at `app.command.chitty.cc` are sibling consumers of the same `meta/executors/*` registry. Legal matter bootstrap is projected through the registered `legal_matter_bootstrap` executor and reads ChittyCounsel without mutating governed evidence or facts. ### Dependencies diff --git a/meta/executors/index.ts b/meta/executors/index.ts index f017800..7ef2b33 100644 --- a/meta/executors/index.ts +++ b/meta/executors/index.ts @@ -13,3 +13,4 @@ export { dispatch } from './dispatch'; // Side-effect imports: each executor file calls registerExecutor() at top level. import './update-obligation-status'; +import './legal-matter-bootstrap'; diff --git a/meta/executors/legal-matter-bootstrap.ts b/meta/executors/legal-matter-bootstrap.ts new file mode 100644 index 0000000..0ed729e --- /dev/null +++ b/meta/executors/legal-matter-bootstrap.ts @@ -0,0 +1,190 @@ +/** + * Executor: legal_matter_bootstrap + * + * Read-only bootstrap projection for a governed legal matter. Delegates case + * discovery/readiness to ChittyCounsel and returns the result for the dispatcher + * to record in cc_actions_log. It never writes evidence, facts, or matter state. + * + * Canonical URI: + * chittycanon://core/services/chittycommand/executors/legal_matter_bootstrap + */ + +import { z } from 'zod'; +import type { ExecutorContext, ExecutorRunOutput, IntentExecutor } from './types'; +import { registerExecutor } from './registry'; + +export const LEGAL_MATTER_BOOTSTRAP_INTENT = 'legal_matter_bootstrap'; + +const DEFAULT_COUNSEL_URL = 'https://counsel.chitty.cc'; +const BOOTSTRAP_TIMEOUT_MS = 10_000; + +export const legalMatterBootstrapSchema = z.object({ + case_chitty_id: z.string().min(1).max(128), +}); + +export type LegalMatterBootstrapArgs = z.infer; + +interface BootstrapFetchOptions { + counselUrl?: string; + fetchFn?: typeof fetch; +} + +export async function runLegalMatterBootstrap( + args: LegalMatterBootstrapArgs, + options: BootstrapFetchOptions = {}, +): Promise<{ + success: boolean; + found?: boolean; + bootstrap?: Record; + case?: Record | null; + provenance?: Record; + error?: string; + upstream_status?: number; +}> { + const counselUrl = (options.counselUrl || DEFAULT_COUNSEL_URL).replace(/\/$/, ''); + const fetchFn = options.fetchFn ?? fetch; + const url = + `${counselUrl}/case/${encodeURIComponent(args.case_chitty_id)}/bootstrap`; + + let response: Response; + try { + response = await fetchFn(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'X-ChittyOS-Caller': 'chittycommand', + }, + signal: AbortSignal.timeout(BOOTSTRAP_TIMEOUT_MS), + }); + } catch (err) { + return { + success: false, + error: `ChittyCounsel unavailable: ${err instanceof Error ? err.message : String(err)}`, + }; + } + + let body: unknown; + try { + body = await response.json(); + } catch { + return { + success: false, + upstream_status: response.status, + error: 'ChittyCounsel returned non-JSON bootstrap response', + }; + } + + if (!response.ok) { + const upstream = body as Record; + return { + success: false, + upstream_status: response.status, + error: + typeof upstream?.error === 'string' + ? upstream.error + : `ChittyCounsel bootstrap failed with HTTP ${response.status}`, + }; + } + + const parsed = z.object({ + found: z.boolean(), + bootstrap: z.record(z.string(), z.unknown()), + case: z.record(z.string(), z.unknown()).nullable().optional(), + provenance: z.record(z.string(), z.unknown()), + }).safeParse(body); + + if (!parsed.success) { + return { + success: false, + upstream_status: response.status, + error: 'ChittyCounsel bootstrap response failed contract validation', + }; + } + + return { + success: true, + found: parsed.data.found, + bootstrap: parsed.data.bootstrap, + case: parsed.data.case ?? null, + provenance: parsed.data.provenance, + }; +} + +const executor: IntentExecutor = { + intentType: LEGAL_MATTER_BOOTSTRAP_INTENT, + canonicalUri: + 'chittycanon://core/services/chittycommand/executors/legal_matter_bootstrap', + + async run(ctx: ExecutorContext): Promise { + if (ctx.intent.privilege !== 'privileged' || ctx.intent.space !== 'legalink') { + return { + ok: false, + description: 'Legal matter bootstrap refused: intent must be privileged + legalink', + actionType: 'legal_matter_bootstrap', + targetType: 'case', + targetId: null, + status: 'failed', + errorMessage: 'classification mismatch: expected privilege=privileged and space=legalink', + }; + } + + const parsed = legalMatterBootstrapSchema.safeParse(ctx.intent.payload); + if (!parsed.success) { + return { + ok: false, + description: `payload validation failed for intent ${ctx.intent.id}`, + actionType: 'legal_matter_bootstrap', + targetType: 'case', + targetId: null, + status: 'failed', + errorMessage: parsed.error.message, + }; + } + + const env = ctx.env as typeof ctx.env & { CHITTYCOUNSEL_URL?: string }; + const result = await runLegalMatterBootstrap(parsed.data, { + counselUrl: env.CHITTYCOUNSEL_URL, + }); + + if (!result.success) { + return { + ok: false, + description: result.error ?? 'ChittyCounsel bootstrap failed', + actionType: 'legal_matter_bootstrap', + targetType: 'case', + targetId: null, + status: 'failed', + errorMessage: result.error ?? 'ChittyCounsel bootstrap failed', + responsePayload: result.upstream_status + ? { upstream_status: result.upstream_status } + : undefined, + }; + } + + return { + ok: true, + description: `Read-only legal matter bootstrap completed for ${parsed.data.case_chitty_id}`, + actionType: 'legal_matter_bootstrap', + targetType: 'case', + targetId: null, + status: 'completed', + responsePayload: { + found: result.found, + bootstrap: result.bootstrap, + case: result.case, + provenance: result.provenance, + }, + metadata: { + source_service: 'chittycounsel', + case_chitty_id: parsed.data.case_chitty_id, + read_only: true, + privilege: 'privileged', + space: 'legalink', + }, + }; + }, +}; + +registerExecutor(executor); + +export default executor; diff --git a/src/index.ts b/src/index.ts index bfbf63f..4f4057e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,6 +78,7 @@ export type Env = { CHITTYSCHEMA_URL?: string; CHITTYCERT_URL?: string; CHITTYEVIDENCE_URL?: string; + CHITTYCOUNSEL_URL?: string; CHITTYEVIDENCE_TOKEN?: string; CHITTY_CONNECT_TOKEN?: string; CHITTYLEDGER_TOKEN?: string; diff --git a/tests/meta/legal-matter-bootstrap.spec.ts b/tests/meta/legal-matter-bootstrap.spec.ts new file mode 100644 index 0000000..d750fa8 --- /dev/null +++ b/tests/meta/legal-matter-bootstrap.spec.ts @@ -0,0 +1,118 @@ +import { describe, expect, it, vi } from 'vitest'; +import executor, { + LEGAL_MATTER_BOOTSTRAP_INTENT, + runLegalMatterBootstrap, +} from '../../meta/executors/legal-matter-bootstrap'; +import type { ExecutorContext } from '../../meta/executors/types'; + +describe('legal_matter_bootstrap executor', () => { + it('returns a validated read-only ChittyCounsel bootstrap response', async () => { + const fetchFn = vi.fn(async (url: string | URL | Request) => { + expect(String(url)).toContain('/case/03-1-USA-0010-L-2602-0-01/bootstrap'); + return Response.json({ + found: true, + bootstrap: { + case_found: true, + phases: ['BOOTSTRAP', 'DISCOVER'], + queues: ['INITIAL_ANALYSIS'], + }, + case: { chitty_id: '03-1-USA-0010-L-2602-0-01' }, + provenance: { worker: 'chittycounsel', version: '0.2.0' }, + }); + }) as typeof fetch; + + const result = await runLegalMatterBootstrap( + { case_chitty_id: '03-1-USA-0010-L-2602-0-01' }, + { counselUrl: 'https://counsel.example.test/', fetchFn }, + ); + + expect(result.success).toBe(true); + expect(result.found).toBe(true); + expect(result.provenance).toMatchObject({ worker: 'chittycounsel' }); + expect(fetchFn).toHaveBeenCalledOnce(); + }); + + it('fails closed on upstream transport failure', async () => { + const fetchFn = vi.fn(async () => { + throw new Error('network down'); + }) as unknown as typeof fetch; + + const result = await runLegalMatterBootstrap( + { case_chitty_id: '03-1-USA-0010-L-2602-0-01' }, + { fetchFn }, + ); + + expect(result.success).toBe(false); + expect(result.error).toContain('ChittyCounsel unavailable'); + }); + + it('refuses intents outside privileged + legalink classification', async () => { + const ctx = { + intent: { + id: 'intent-test', + intentType: LEGAL_MATTER_BOOTSTRAP_INTENT, + payload: { case_chitty_id: '03-1-USA-0010-L-2602-0-01' }, + privilege: 'public', + space: 'business', + }, + } as unknown as ExecutorContext; + + const result = await executor.run(ctx); + + expect(result.ok).toBe(false); + expect(result.status).toBe('failed'); + expect(result.errorMessage).toContain('classification mismatch'); + }); + + it('keeps the case ChittyID in metadata rather than UUID-only audit target_id', async () => { + const fetchMock = vi.fn(async () => Response.json({ + found: true, + bootstrap: { case_found: true }, + case: { chitty_id: '03-1-USA-0010-L-2602-0-01' }, + provenance: { worker: 'chittycounsel', version: '0.2.0' }, + })); + vi.stubGlobal('fetch', fetchMock); + + const ctx = { + intent: { + id: 'intent-test', + intentType: LEGAL_MATTER_BOOTSTRAP_INTENT, + payload: { case_chitty_id: '03-1-USA-0010-L-2602-0-01' }, + privilege: 'privileged', + space: 'legalink', + }, + env: { CHITTYCOUNSEL_URL: 'https://counsel.example.test' }, + } as unknown as ExecutorContext; + + const result = await executor.run(ctx); + + expect(result.ok).toBe(true); + expect(result.targetId).toBeNull(); + expect(result.metadata).toMatchObject({ + case_chitty_id: '03-1-USA-0010-L-2602-0-01', + source_service: 'chittycounsel', + read_only: true, + }); + + vi.unstubAllGlobals(); + }); + + it('rejects an invalid payload before any upstream call', async () => { + const ctx = { + intent: { + id: 'intent-test', + intentType: LEGAL_MATTER_BOOTSTRAP_INTENT, + payload: {}, + privilege: 'privileged', + space: 'legalink', + }, + env: {}, + } as unknown as ExecutorContext; + + const result = await executor.run(ctx); + + expect(result.ok).toBe(false); + expect(result.status).toBe('failed'); + expect(result.errorMessage).toContain('case_chitty_id'); + }); +}); diff --git a/wrangler.jsonc b/wrangler.jsonc index dc3fc2a..d057ee5 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -25,6 +25,7 @@ "CHITTYROUTER_URL": "https://router.chitty.cc", "CHITTYAGENT_SCRAPE_URL": "https://chittyagent-scrape.ccorp.workers.dev", "CHITTYEVIDENCE_URL": "https://evidence.chitty.cc", + "CHITTYCOUNSEL_URL": "https://counsel.chitty.cc", "CHITTYGOV_URL": "https://gov.chitty.cc", "CHITTYREGISTER_URL": "https://register.chitty.cc" // PLAID_CLIENT_ID and PLAID_SECRET set via `wrangler secret put`