From 7efea7a0f9d094c69ed61e9f4e3d068bc40520fa Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:21:24 -0500 Subject: [PATCH 01/21] feat(runtime): add channel-neutral execution context --- server/middleware/execution-context.ts | 122 +++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 server/middleware/execution-context.ts diff --git a/server/middleware/execution-context.ts b/server/middleware/execution-context.ts new file mode 100644 index 0000000..fa44140 --- /dev/null +++ b/server/middleware/execution-context.ts @@ -0,0 +1,122 @@ +import type { Context, MiddlewareHandler } from 'hono'; +import type { HonoEnv } from '../env'; + +export type ExecutionIntent = 'read' | 'suggest' | 'preview' | 'execute'; + +export interface FinanceExecutionContext { + actor: { + userId: string; + authMethod: 'service' | 'chittyauth' | 'session'; + }; + source: { + service: string; + channel?: string; + workspace?: string; + session?: string; + }; + scope: { + tenantId: string; + }; + capability: string; + intent: ExecutionIntent; + trace: { + requestId: string; + traceparent?: string; + }; +} + +function parseBaggage(raw?: string): Map { + const result = new Map(); + if (!raw) return result; + + for (const member of raw.split(',')) { + const pair = member.trim().split(';', 1)[0]; + const separator = pair.indexOf('='); + if (separator <= 0) continue; + + const key = pair.slice(0, separator).trim(); + const value = pair.slice(separator + 1).trim(); + if (!key || !value) continue; + + try { + result.set(key, decodeURIComponent(value)); + } catch { + result.set(key, value); + } + } + + return result; +} + +function normalizePath(pathname: string): string { + return pathname + .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi, ':id') + .replace(/\/\d+(?=\/|$)/g, '/:id'); +} + +export function inferExecutionIntent(method: string, pathname: string): ExecutionIntent { + const upper = method.toUpperCase(); + if (upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS') return 'read'; + if (pathname.includes('/preview')) return 'preview'; + if (pathname.includes('/suggest') || pathname.includes('/advice')) return 'suggest'; + return 'execute'; +} + +function defaultCapability(method: string, pathname: string): string { + return `finance.http.${method.toLowerCase()}:${normalizePath(pathname)}`; +} + +/** + * Build channel-neutral execution provenance after actor + tenant authorization. + * + * Authorization never depends on source/channel/workspace/session metadata. + * X-Source-Service and W3C baggage are provenance only. + */ +export const executionContextMiddleware: MiddlewareHandler = async (c, next) => { + const baggage = parseBaggage(c.req.header('baggage')); + const sourceService = + c.req.header('x-source-service') ?? + baggage.get('chitty.source') ?? + 'finance.chitty.cc'; + + const context: FinanceExecutionContext = { + actor: { + userId: c.get('userId'), + authMethod: c.get('authMethod'), + }, + source: { + service: sourceService, + channel: baggage.get('chitty.channel') || undefined, + workspace: baggage.get('chitty.workspace') || undefined, + session: baggage.get('chitty.session') || undefined, + }, + scope: { + tenantId: c.get('tenantId'), + }, + capability: defaultCapability(c.req.method, new URL(c.req.url).pathname), + intent: inferExecutionIntent(c.req.method, new URL(c.req.url).pathname), + trace: { + requestId: crypto.randomUUID(), + traceparent: c.req.header('traceparent') || undefined, + }, + }; + + c.set('executionContext', context); + await next(); +}; + +export function setExecutionOperation( + c: Context, + capability: string, + intent: ExecutionIntent, +): FinanceExecutionContext { + const current = c.get('executionContext'); + const updated = { ...current, capability, intent }; + c.set('executionContext', updated); + return updated; +} + +export function executionAuditMetadata(c: { get(name: 'executionContext'): FinanceExecutionContext }) { + const execution = c.get('executionContext'); + return execution ? { execution } : {}; +} From 21b56a770a8598a5538e65b891079427075680ac Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:21:41 -0500 Subject: [PATCH 02/21] feat(runtime): mount execution context after authorization --- server/app.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/app.ts b/server/app.ts index 9aa392a..0cdfd32 100644 --- a/server/app.ts +++ b/server/app.ts @@ -8,6 +8,7 @@ import { hybridAuth, serviceAuth } from './middleware/auth'; import { sessionRoutes } from './routes/session'; import { callerContext } from './middleware/caller'; import { tenantMiddleware } from './middleware/tenant'; +import { executionContextMiddleware } from './middleware/execution-context'; import { healthRoutes } from './routes/health'; import { docRoutes } from './routes/docs'; import { accountRoutes } from './accounting/accounts'; @@ -73,7 +74,7 @@ export function createApp(deps: AppDeps = {}) { // storageMiddleware runs first so hybridAuth can resolve JWT → chittyId → userId const authAndContext: MiddlewareHandler[] = [storageMiddleware, hybridAuth, callerContext]; - const protectedRoute: MiddlewareHandler[] = [...authAndContext, tenantMiddleware]; + const protectedRoute: MiddlewareHandler[] = [...authAndContext, tenantMiddleware, executionContextMiddleware]; const app = new Hono(); @@ -84,7 +85,7 @@ export function createApp(deps: AppDeps = {}) { app.use('*', cors({ origin: ['https://app.command.chitty.cc', 'https://command.chitty.cc', 'https://finance.chitty.cc', 'http://localhost:5000', 'http://localhost:3000'], allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization', 'X-Tenant-ID', 'X-Source-Service', 'X-Account-ID', 'Stripe-Signature'], + allowHeaders: ['Content-Type', 'Authorization', 'X-Tenant-ID', 'X-Source-Service', 'X-Account-ID', 'Stripe-Signature', 'traceparent', 'baggage'], credentials: true, })); From 61e758d912d944754484bfd41346e86d96efd6e4 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:21:43 -0500 Subject: [PATCH 03/21] feat(runtime): type execution context in Hono variables --- server/env.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/env.ts b/server/env.ts index f9e6aa8..57a714a 100644 --- a/server/env.ts +++ b/server/env.ts @@ -54,11 +54,13 @@ export interface Env { import type { SystemStorage } from './storage/system'; import type { Database } from './db/connection'; +import type { FinanceExecutionContext } from './middleware/execution-context'; export interface Variables { tenantId: string; userId: string; authMethod: 'service' | 'chittyauth' | 'session'; + executionContext: FinanceExecutionContext; storage: SystemStorage; /** * The same drizzle handle SystemStorage was built on. Routes go through storage From 0b49f1045cac9bccad7b57e6e3efdded1b8f06b4 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:21:45 -0500 Subject: [PATCH 04/21] feat(audit): attach execution provenance to ledger writes --- server/lib/ledger-client.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/lib/ledger-client.ts b/server/lib/ledger-client.ts index b41be9c..e13acf4 100644 --- a/server/lib/ledger-client.ts +++ b/server/lib/ledger-client.ts @@ -110,8 +110,19 @@ export async function logToLedger(entry: LedgerEntry, env: LedgerEnv): Promise): void } }, entry: LedgerEntry, env: LedgerEnv): void { - const promise = logToLedger(entry, env); +export function ledgerLog( + c: { + executionCtx: { waitUntil(p: Promise): void }; + get?: (name: 'executionContext') => any; + }, + entry: LedgerEntry, + env: LedgerEnv, +): void { + const execution = typeof c.get === 'function' ? c.get('executionContext') : undefined; + const enrichedEntry = execution + ? { ...entry, metadata: { ...(entry.metadata ?? {}), execution } } + : entry; + const promise = logToLedger(enrichedEntry, env); try { c.executionCtx.waitUntil(promise); } catch { From 8993c9b2d735186589626abc1d15367bb4709d62 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:21:53 -0500 Subject: [PATCH 05/21] feat(mcp): label canonical capability and intent --- server/routes/mcp.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/routes/mcp.ts b/server/routes/mcp.ts index a4d6be8..7feea3e 100644 --- a/server/routes/mcp.ts +++ b/server/routes/mcp.ts @@ -16,6 +16,7 @@ import { Hono } from 'hono'; import type { HonoEnv } from '../env'; +import { setExecutionOperation } from '../middleware/execution-context'; export const mcpRoutes = new Hono(); @@ -263,6 +264,7 @@ mcpRoutes.post('/mcp', async (c) => { try { switch (body.method) { case 'initialize': + setExecutionOperation(c, 'finance.mcp.initialize', 'read'); return c.json(rpcOk(body.id, { protocolVersion: '2024-11-05', serverInfo: SERVER_INFO, @@ -270,22 +272,27 @@ mcpRoutes.post('/mcp', async (c) => { })); case 'resources/list': + setExecutionOperation(c, 'finance.mcp.resources.list', 'read'); return c.json(rpcOk(body.id, { resources: RESOURCES })); case 'resources/read': { const uri = body.params?.uri; if (!uri) return c.json(rpcError(body.id, -32602, 'Missing uri param'), 400); + setExecutionOperation(c, `finance.mcp.resources.read:${uri}`, 'read'); const result = await readResource(uri, storage, tenantId, userId); return c.json(rpcOk(body.id, result)); } case 'tools/list': + setExecutionOperation(c, 'finance.mcp.tools.list', 'read'); return c.json(rpcOk(body.id, { tools: TOOLS })); case 'tools/call': { const toolName = body.params?.name; const toolArgs = body.params?.arguments || {}; if (!toolName) return c.json(rpcError(body.id, -32602, 'Missing tool name'), 400); + const intent = toolName === 'get-property-advice' ? 'suggest' : 'execute'; + setExecutionOperation(c, `finance.mcp.tool:${toolName}`, intent); const result = await callTool(toolName, toolArgs, storage, tenantId, c.env); return c.json(rpcOk(body.id, result)); } From dffa4d5aff84dc80e6a85c508ba322e69ef1231b Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:22:12 -0500 Subject: [PATCH 06/21] test(runtime): prove channel-neutral execution context --- server/__tests__/execution-context.test.ts | 96 ++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 server/__tests__/execution-context.test.ts diff --git a/server/__tests__/execution-context.test.ts b/server/__tests__/execution-context.test.ts new file mode 100644 index 0000000..94352bc --- /dev/null +++ b/server/__tests__/execution-context.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { Hono } from 'hono'; +import type { HonoEnv } from '../env'; +import { + executionContextMiddleware, + inferExecutionIntent, + setExecutionOperation, +} from '../middleware/execution-context'; + +function buildApp() { + const app = new Hono(); + + app.use('*', async (c, next) => { + c.set('userId', 'user-1'); + c.set('authMethod', 'chittyauth'); + c.set('tenantId', 'tenant-authorized'); + await next(); + }); + app.use('*', executionContextMiddleware); + + app.get('/api/test', (c) => c.json(c.get('executionContext'))); + app.post('/api/allocations/preview', (c) => c.json(c.get('executionContext'))); + app.post('/api/allocations/execute', (c) => c.json(c.get('executionContext'))); + app.post('/mcp-test', (c) => { + setExecutionOperation(c, 'finance.mcp.tool:get-property-advice', 'suggest'); + return c.json(c.get('executionContext')); + }); + + return app; +} + +describe('execution context', () => { + it('infers read, preview, suggest, and execute mechanically', () => { + expect(inferExecutionIntent('GET', '/api/accounts')).toBe('read'); + expect(inferExecutionIntent('POST', '/api/allocations/preview')).toBe('preview'); + expect(inferExecutionIntent('POST', '/api/classification/suggest')).toBe('suggest'); + expect(inferExecutionIntent('POST', '/api/allocations/execute')).toBe('execute'); + }); + + it('captures channel-neutral provenance from existing source header and W3C baggage', async () => { + const app = buildApp(); + const res = await app.request('/api/test', { + headers: { + 'X-Source-Service': 'chittyclaw', + baggage: 'chitty.channel=slack,chitty.workspace=workspace-1,chitty.session=session-1', + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + }, + }); + const body = await res.json() as any; + + expect(body.actor).toEqual({ userId: 'user-1', authMethod: 'chittyauth' }); + expect(body.source).toEqual({ + service: 'chittyclaw', + channel: 'slack', + workspace: 'workspace-1', + session: 'session-1', + }); + expect(body.scope).toEqual({ tenantId: 'tenant-authorized' }); + expect(body.intent).toBe('read'); + expect(body.trace.traceparent).toContain('4bf92f3577b34da6a3ce929d0e0e4736'); + expect(body.trace.requestId).toBeTruthy(); + }); + + it('does not derive financial scope from source metadata', async () => { + const app = buildApp(); + const res = await app.request('/api/test', { + headers: { + baggage: 'chitty.workspace=tenant-attacker,chitty.session=tenant-other', + }, + }); + const body = await res.json() as any; + + expect(body.scope.tenantId).toBe('tenant-authorized'); + expect(body.source.workspace).toBe('tenant-attacker'); + }); + + it('marks preview and execute routes distinctly', async () => { + const app = buildApp(); + + const preview = await (await app.request('/api/allocations/preview', { method: 'POST' })).json() as any; + const execute = await (await app.request('/api/allocations/execute', { method: 'POST' })).json() as any; + + expect(preview.intent).toBe('preview'); + expect(execute.intent).toBe('execute'); + }); + + it('allows MCP to override the generic HTTP operation with canonical capability intent', async () => { + const app = buildApp(); + const body = await (await app.request('/mcp-test', { method: 'POST' })).json() as any; + + expect(body.capability).toBe('finance.mcp.tool:get-property-advice'); + expect(body.intent).toBe('suggest'); + expect(body.actor.userId).toBe('user-1'); + expect(body.scope.tenantId).toBe('tenant-authorized'); + }); +}); From 74cd44e6ac16a2e3366003fee8222c57cfd3e355 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:22:54 -0500 Subject: [PATCH 07/21] docs(agents): define channel-neutral execution context --- AGENTS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 11e5343..72d382b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ Mounted at `/mcp`. Resources include: - Allocation rule preview - Schedule E line summary -Authentication: verified ChittyAuth bearer JWT for end-user/agent callers; legacy internal service-token callers remain supported with explicit `X-Chitty-User-Id`. Tenant authorization still comes from `tenant_users`. See [SECURITY.md](SECURITY.md). +Authentication: verified ChittyAuth bearer JWT for end-user/agent callers; legacy internal service-token callers remain supported with explicit `X-Chitty-User-Id`. Tenant authorization still comes from `tenant_users`. After authorization, every protected request receives a channel-neutral execution context carrying actor, tenant scope, capability/intent, source provenance, and trace metadata. Source metadata never grants financial authority. See [SECURITY.md](SECURITY.md). > ⚠️ **Phase 2 remaining**: ChittyConnect MCP integration not yet wired. Internal MCP routes work today; cross-service MCP discovery via ChittyConnect is pending. @@ -83,6 +83,10 @@ When working in this repo, prefer these subagents (see user's `~/.claude/agents/ - COA modifications (L4) — only `tenant_users.role` ∈ {owner, admin} - Webhook signature verification — never bypassed for any agent caller +### Channel-Neutral Execution Context + +Protected requests are normalized after authentication + tenant membership checks. The runtime context is request-scoped only (no new database/schema) and contains the verified actor, authorized tenant scope, inferred or route-specific capability/intent, source provenance, and trace metadata. `X-Source-Service` plus W3C `traceparent`/`baggage` carry portable provenance for ChatGPT, Claude, ChittyClaw/OpenClaw, and other adapters. Platform workspace/channel metadata is audit context only and MUST NOT affect `tenant_users` authorization. + **ChittyFinance does delegate these to external agents:** - Identity (ChittyID via OAuth 2.0 PKCE) - Token validation (ChittyAuth) From 4c3dd022092605c76960495bd286b74ccb212354 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:22:57 -0500 Subject: [PATCH 08/21] docs(security): document cross-channel provenance boundary --- SECURITY.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index ab1f54c..4524462 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -47,6 +47,7 @@ We follow coordinated disclosure and will credit reporters unless anonymity is p - MCP `finance://tenants` enumerates only the authenticated caller's memberships - All storage methods enforce `tenantId` filtering - Inter-tenant data access prevented at the storage abstraction layer +- Channel/workspace/session provenance is resolved only after actor + tenant authorization and cannot select or elevate tenant access ### Secret Management @@ -56,6 +57,13 @@ We follow coordinated disclosure and will credit reporters unless anonymity is p - No secrets in code, KV, or R2 - Pre-commit hooks scan for credential patterns +### Cross-Channel Provenance + +- Existing `X-Source-Service` identifies the calling ChittyOS/platform adapter for audit purposes +- W3C `traceparent` carries distributed trace linkage; W3C `baggage` may carry `chitty.source`, `chitty.channel`, `chitty.workspace`, and `chitty.session` +- Provenance fields are untrusted authorization inputs: they are never used to derive `userId`, tenant membership, or financial role +- ChittyLedger audit writes inherit the request execution context automatically + ### OAuth & Webhook Security - **OAuth state**: HMAC-SHA256 signed tokens, 10-minute expiry, timing-safe verification From c5006072d494210319e6fb859f2829beedfa3a44 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:23:00 -0500 Subject: [PATCH 09/21] docs(charter): declare portable execution context --- CHARTER.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHARTER.md b/CHARTER.md index 5796ef2..a28611b 100644 --- a/CHARTER.md +++ b/CHARTER.md @@ -42,6 +42,7 @@ ChittyFinance is a **full-stack financial management platform** for the ChittyOS - Forensic accounting (Benford's Law, duplicate detection, flow of funds, damages calculation) - GitHub integration for project cost attribution - Tenant-scoped financial data isolation +- Channel-neutral request execution context for ChatGPT, Claude, ChittyClaw/OpenClaw, and other adapters (actor + authorized scope + capability/intent + provenance + trace; request-scoped only) - Inbound email handling at `finance@chitty.cc` (Cloudflare Email Service) ### IS NOT Responsible For @@ -114,7 +115,7 @@ IT CAN BE LLC (holding) ### MCP | Endpoint | Method | Purpose | |----------|--------|---------| -| `/mcp` | POST | JSON-RPC MCP resources/tools; caller identity via ChittyAuth bearer JWT or legacy service-token compatibility lane; tenant authorization remains membership-scoped | +| `/mcp` | POST | JSON-RPC MCP resources/tools; caller identity via ChittyAuth bearer JWT or legacy service-token compatibility lane; tenant authorization remains membership-scoped; MCP methods label canonical capability + read/suggest/execute intent | ### Financial Data | Endpoint | Method | Purpose | From f6923b4d53e157e4167afda193e85348dcfaa709 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:23:02 -0500 Subject: [PATCH 10/21] docs(chitty): describe provider-neutral runtime context --- CHITTY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHITTY.md b/CHITTY.md index 11cde60..3c1aa2f 100644 --- a/CHITTY.md +++ b/CHITTY.md @@ -20,7 +20,7 @@ Full-stack financial management platform providing intelligent tracking, AI-powe ## Architecture -Dual-mode: Hono on Cloudflare Workers (production) with Neon PostgreSQL multi-tenant, or Hono via `@hono/node-server` with SQLite (local dev). React frontend with Vite. +Dual-mode: Hono on Cloudflare Workers (production) with Neon PostgreSQL multi-tenant, or Hono via `@hono/node-server` with SQLite (local dev). React frontend with Vite. Protected requests are normalized into a request-scoped execution context after identity and tenant authorization so ChatGPT, Claude, ChittyClaw/OpenClaw, and web clients can share the same financial capability surface without sharing platform-specific authority logic. ### Stack - **Runtime**: Cloudflare Workers + Hono (production) / Hono node-server (dev) / Express (legacy `dev:legacy` fallback) From 6d9d509bf6a3ed9e36bb63d2f3853fe5189641d1 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:24:34 -0500 Subject: [PATCH 11/21] fix(runtime): sanitize claimed provenance and intent inference --- server/middleware/execution-context.ts | 65 ++++++++++++++++++-------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/server/middleware/execution-context.ts b/server/middleware/execution-context.ts index fa44140..51e089c 100644 --- a/server/middleware/execution-context.ts +++ b/server/middleware/execution-context.ts @@ -10,6 +10,7 @@ export interface FinanceExecutionContext { }; source: { service: string; + claimed: true; channel?: string; workspace?: string; session?: string; @@ -25,9 +26,29 @@ export interface FinanceExecutionContext { }; } +const MAX_BAGGAGE_BYTES = 8192; +const MAX_PROVENANCE_VALUE = 128; +const TRACEPARENT_RE = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/; +const PRINTABLE_RE = /^[\x20-\x7E]+$/; + +function sanitizeValue(value?: string | null): string | undefined { + if (!value) return undefined; + const trimmed = value.trim(); + if (!trimmed || trimmed.length > MAX_PROVENANCE_VALUE || !PRINTABLE_RE.test(trimmed)) { + return undefined; + } + return trimmed; +} + +function sanitizeTraceparent(value?: string | null): string | undefined { + if (!value) return undefined; + const normalized = value.trim().toLowerCase(); + return TRACEPARENT_RE.test(normalized) ? normalized : undefined; +} + function parseBaggage(raw?: string): Map { const result = new Map(); - if (!raw) return result; + if (!raw || new TextEncoder().encode(raw).byteLength > MAX_BAGGAGE_BYTES) return result; for (const member of raw.split(',')) { const pair = member.trim().split(';', 1)[0]; @@ -35,14 +56,18 @@ function parseBaggage(raw?: string): Map { if (separator <= 0) continue; const key = pair.slice(0, separator).trim(); - const value = pair.slice(separator + 1).trim(); - if (!key || !value) continue; + const encodedValue = pair.slice(separator + 1).trim(); + if (!key || !encodedValue) continue; + let decoded = encodedValue; try { - result.set(key, decodeURIComponent(value)); + decoded = decodeURIComponent(encodedValue); } catch { - result.set(key, value); + continue; } + + const safe = sanitizeValue(decoded); + if (safe) result.set(key, safe); } return result; @@ -54,11 +79,15 @@ function normalizePath(pathname: string): string { .replace(/\/\d+(?=\/|$)/g, '/:id'); } +function hasPathSegment(pathname: string, segment: string): boolean { + return pathname.split('/').filter(Boolean).includes(segment); +} + export function inferExecutionIntent(method: string, pathname: string): ExecutionIntent { const upper = method.toUpperCase(); if (upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS') return 'read'; - if (pathname.includes('/preview')) return 'preview'; - if (pathname.includes('/suggest') || pathname.includes('/advice')) return 'suggest'; + if (hasPathSegment(pathname, 'preview')) return 'preview'; + if (hasPathSegment(pathname, 'suggest') || hasPathSegment(pathname, 'advice')) return 'suggest'; return 'execute'; } @@ -69,15 +98,17 @@ function defaultCapability(method: string, pathname: string): string { /** * Build channel-neutral execution provenance after actor + tenant authorization. * - * Authorization never depends on source/channel/workspace/session metadata. - * X-Source-Service and W3C baggage are provenance only. + * Source/channel/workspace/session values are caller claims used for provenance only. + * Authorization never depends on them. */ export const executionContextMiddleware: MiddlewareHandler = async (c, next) => { const baggage = parseBaggage(c.req.header('baggage')); const sourceService = - c.req.header('x-source-service') ?? + sanitizeValue(c.req.header('x-source-service')) ?? baggage.get('chitty.source') ?? 'finance.chitty.cc'; + const pathname = c.req.path; + const traceparent = sanitizeTraceparent(c.req.header('traceparent')); const context: FinanceExecutionContext = { actor: { @@ -86,6 +117,7 @@ export const executionContextMiddleware: MiddlewareHandler = async (c, }, source: { service: sourceService, + claimed: true, channel: baggage.get('chitty.channel') || undefined, workspace: baggage.get('chitty.workspace') || undefined, session: baggage.get('chitty.session') || undefined, @@ -93,11 +125,11 @@ export const executionContextMiddleware: MiddlewareHandler = async (c, scope: { tenantId: c.get('tenantId'), }, - capability: defaultCapability(c.req.method, new URL(c.req.url).pathname), - intent: inferExecutionIntent(c.req.method, new URL(c.req.url).pathname), + capability: defaultCapability(c.req.method, pathname), + intent: inferExecutionIntent(c.req.method, pathname), trace: { requestId: crypto.randomUUID(), - traceparent: c.req.header('traceparent') || undefined, + traceparent, }, }; @@ -111,12 +143,7 @@ export function setExecutionOperation( intent: ExecutionIntent, ): FinanceExecutionContext { const current = c.get('executionContext'); - const updated = { ...current, capability, intent }; + const updated = { ...current, capability: sanitizeValue(capability) ?? 'finance.unknown', intent }; c.set('executionContext', updated); return updated; } - -export function executionAuditMetadata(c: { get(name: 'executionContext'): FinanceExecutionContext }) { - const execution = c.get('executionContext'); - return execution ? { execution } : {}; -} From 055c2a63bbf26734c3d08bd05d1977ec363b9d13 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:24:44 -0500 Subject: [PATCH 12/21] fix(audit): type execution provenance enrichment --- server/lib/ledger-client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/lib/ledger-client.ts b/server/lib/ledger-client.ts index e13acf4..c7b354d 100644 --- a/server/lib/ledger-client.ts +++ b/server/lib/ledger-client.ts @@ -1,3 +1,5 @@ +import type { FinanceExecutionContext } from '../middleware/execution-context'; + /** * ChittyLedger client for immutable audit trail entries. * 100% Cloudflare Workers compatible — no Node.js APIs, no process.env. @@ -113,7 +115,7 @@ export async function logToLedger(entry: LedgerEntry, env: LedgerEnv): Promise): void }; - get?: (name: 'executionContext') => any; + get?: (name: 'executionContext') => FinanceExecutionContext | undefined; }, entry: LedgerEntry, env: LedgerEnv, From b5b498a622e1ca2ffe8bb89a6ad5f514dd6b7b8e Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:24:57 -0500 Subject: [PATCH 13/21] fix(mcp): declare execution intent per tool --- server/routes/mcp.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/server/routes/mcp.ts b/server/routes/mcp.ts index 7feea3e..0e16913 100644 --- a/server/routes/mcp.ts +++ b/server/routes/mcp.ts @@ -16,7 +16,7 @@ import { Hono } from 'hono'; import type { HonoEnv } from '../env'; -import { setExecutionOperation } from '../middleware/execution-context'; +import { setExecutionOperation, type ExecutionIntent } from '../middleware/execution-context'; export const mcpRoutes = new Hono(); @@ -70,10 +70,18 @@ const RESOURCES = [ }, ]; -const TOOLS = [ +interface McpToolDefinition { + name: string; + description: string; + intent: ExecutionIntent; + inputSchema: Record; +} + +const TOOL_DEFINITIONS: McpToolDefinition[] = [ { name: 'get-property-advice', description: 'Get AI-powered financial advice for a specific property.', + intent: 'suggest', inputSchema: { type: 'object' as const, properties: { @@ -86,6 +94,7 @@ const TOOLS = [ { name: 'refresh-valuation', description: 'Refresh property valuation estimates from external providers (Zillow, Redfin, HouseCanary, ATTOM, County).', + intent: 'execute', inputSchema: { type: 'object' as const, properties: { @@ -96,6 +105,8 @@ const TOOLS = [ }, ]; +const TOOLS = TOOL_DEFINITIONS.map(({ intent: _intent, ...tool }) => tool); + // ── Resource Handlers ── async function readResource(uri: string, storage: any, tenantId: string, userId: string): Promise<{ contents: any[] }> { @@ -291,8 +302,9 @@ mcpRoutes.post('/mcp', async (c) => { const toolName = body.params?.name; const toolArgs = body.params?.arguments || {}; if (!toolName) return c.json(rpcError(body.id, -32602, 'Missing tool name'), 400); - const intent = toolName === 'get-property-advice' ? 'suggest' : 'execute'; - setExecutionOperation(c, `finance.mcp.tool:${toolName}`, intent); + const toolDefinition = TOOL_DEFINITIONS.find((tool) => tool.name === toolName); + if (!toolDefinition) return c.json(rpcError(body.id, -32602, `Unknown tool: ${toolName}`), 400); + setExecutionOperation(c, `finance.mcp.tool:${toolName}`, toolDefinition.intent); const result = await callTool(toolName, toolArgs, storage, tenantId, c.env); return c.json(rpcOk(body.id, result)); } From f54aacdb5241d9fbd200606a9171e32afc206201 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:25:14 -0500 Subject: [PATCH 14/21] test(runtime): cover provenance sanitization and exact intent matching --- server/__tests__/execution-context.test.ts | 32 ++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/server/__tests__/execution-context.test.ts b/server/__tests__/execution-context.test.ts index 94352bc..c61358e 100644 --- a/server/__tests__/execution-context.test.ts +++ b/server/__tests__/execution-context.test.ts @@ -35,14 +35,15 @@ describe('execution context', () => { expect(inferExecutionIntent('POST', '/api/allocations/preview')).toBe('preview'); expect(inferExecutionIntent('POST', '/api/classification/suggest')).toBe('suggest'); expect(inferExecutionIntent('POST', '/api/allocations/execute')).toBe('execute'); + expect(inferExecutionIntent('POST', '/api/x/preview-and-commit')).toBe('execute'); }); - it('captures channel-neutral provenance from existing source header and W3C baggage', async () => { + it('captures sanitized channel-neutral provenance from source header and W3C baggage', async () => { const app = buildApp(); const res = await app.request('/api/test', { headers: { 'X-Source-Service': 'chittyclaw', - baggage: 'chitty.channel=slack,chitty.workspace=workspace-1,chitty.session=session-1', + baggage: 'chitty.channel=slack;prop=1,chitty.workspace=workspace-1,chitty.session=session-1', traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', }, }); @@ -51,6 +52,7 @@ describe('execution context', () => { expect(body.actor).toEqual({ userId: 'user-1', authMethod: 'chittyauth' }); expect(body.source).toEqual({ service: 'chittyclaw', + claimed: true, channel: 'slack', workspace: 'workspace-1', session: 'session-1', @@ -61,6 +63,32 @@ describe('execution context', () => { expect(body.trace.requestId).toBeTruthy(); }); + it('drops malformed or oversized provenance values', async () => { + const app = buildApp(); + const res = await app.request('/api/test', { + headers: { + 'X-Source-Service': 'x'.repeat(129), + baggage: 'chitty.channel=' + 'y'.repeat(9000), + traceparent: 'not-a-traceparent', + }, + }); + const body = await res.json() as any; + + expect(body.source.service).toBe('finance.chitty.cc'); + expect(body.source.channel).toBeUndefined(); + expect(body.trace.traceparent).toBeUndefined(); + }); + + it('uses chitty.source baggage only as a claimed provenance fallback', async () => { + const app = buildApp(); + const body = await (await app.request('/api/test', { + headers: { baggage: 'chitty.source=claude' }, + })).json() as any; + + expect(body.source.service).toBe('claude'); + expect(body.source.claimed).toBe(true); + }); + it('does not derive financial scope from source metadata', async () => { const app = buildApp(); const res = await app.request('/api/test', { From 1b676cf2dfa9e4390e002c335af62044c2e215fb Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:25:50 -0500 Subject: [PATCH 15/21] test(mcp): align unknown-tool validation behavior --- server/__tests__/mcp.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/__tests__/mcp.test.ts b/server/__tests__/mcp.test.ts index 0252a58..18ddde2 100644 --- a/server/__tests__/mcp.test.ts +++ b/server/__tests__/mcp.test.ts @@ -219,14 +219,14 @@ describe('MCP endpoint', () => { expect(body.result.content[0].text).toContain('not found'); }); - it('returns error for unknown tool', async () => { + it('rejects unknown tool before execution', async () => { const res = await rpc(app, 'tools/call', { name: 'nonexistent-tool', arguments: {}, }); - expect(res.status).toBe(500); + expect(res.status).toBe(400); const body = await res.json() as any; - expect(body.error.code).toBe(-32000); + expect(body.error.code).toBe(-32602); }); it('returns error for missing tool name', async () => { From 1074941221e195cb805c0ddf9870e2548756fb1b Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:27:31 -0500 Subject: [PATCH 16/21] fix(runtime): fail safe on intent and distinguish claimed provenance --- server/middleware/execution-context.ts | 28 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/server/middleware/execution-context.ts b/server/middleware/execution-context.ts index 51e089c..7c66707 100644 --- a/server/middleware/execution-context.ts +++ b/server/middleware/execution-context.ts @@ -10,7 +10,7 @@ export interface FinanceExecutionContext { }; source: { service: string; - claimed: true; + claimed: boolean; channel?: string; workspace?: string; session?: string; @@ -79,15 +79,20 @@ function normalizePath(pathname: string): string { .replace(/\/\d+(?=\/|$)/g, '/:id'); } -function hasPathSegment(pathname: string, segment: string): boolean { - return pathname.split('/').filter(Boolean).includes(segment); +function terminalPathSegment(pathname: string): string | undefined { + return pathname.split('/').filter(Boolean).at(-1); } export function inferExecutionIntent(method: string, pathname: string): ExecutionIntent { const upper = method.toUpperCase(); if (upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS') return 'read'; - if (hasPathSegment(pathname, 'preview')) return 'preview'; - if (hasPathSegment(pathname, 'suggest') || hasPathSegment(pathname, 'advice')) return 'suggest'; + + const terminal = terminalPathSegment(pathname); + if (terminal === 'preview') return 'preview'; + if (terminal === 'suggest' || terminal === 'advice') return 'suggest'; + + // Fail safe: mutating HTTP methods are execute unless the route explicitly + // terminates in a known non-authoritative operation label. return 'execute'; } @@ -103,10 +108,9 @@ function defaultCapability(method: string, pathname: string): string { */ export const executionContextMiddleware: MiddlewareHandler = async (c, next) => { const baggage = parseBaggage(c.req.header('baggage')); - const sourceService = - sanitizeValue(c.req.header('x-source-service')) ?? - baggage.get('chitty.source') ?? - 'finance.chitty.cc'; + const headerSource = sanitizeValue(c.req.header('x-source-service')); + const baggageSource = baggage.get('chitty.source'); + const sourceService = headerSource ?? baggageSource ?? 'finance.chitty.cc'; const pathname = c.req.path; const traceparent = sanitizeTraceparent(c.req.header('traceparent')); @@ -117,7 +121,7 @@ export const executionContextMiddleware: MiddlewareHandler = async (c, }, source: { service: sourceService, - claimed: true, + claimed: Boolean(headerSource ?? baggageSource), channel: baggage.get('chitty.channel') || undefined, workspace: baggage.get('chitty.workspace') || undefined, session: baggage.get('chitty.session') || undefined, @@ -128,7 +132,7 @@ export const executionContextMiddleware: MiddlewareHandler = async (c, capability: defaultCapability(c.req.method, pathname), intent: inferExecutionIntent(c.req.method, pathname), trace: { - requestId: crypto.randomUUID(), + requestId: traceparent?.split('-')[1] ?? crypto.randomUUID(), traceparent, }, }; @@ -143,6 +147,8 @@ export function setExecutionOperation( intent: ExecutionIntent, ): FinanceExecutionContext { const current = c.get('executionContext'); + if (!current) throw new Error('execution_context_unavailable'); + const updated = { ...current, capability: sanitizeValue(capability) ?? 'finance.unknown', intent }; c.set('executionContext', updated); return updated; From 458d9a6cbaee52dff366ad0ba95510af1af3f463 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:27:40 -0500 Subject: [PATCH 17/21] fix(mcp): canonicalize resource capabilities and preserve errors --- server/routes/mcp.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/server/routes/mcp.ts b/server/routes/mcp.ts index 0e16913..3414d32 100644 --- a/server/routes/mcp.ts +++ b/server/routes/mcp.ts @@ -49,6 +49,12 @@ const CAPABILITIES = { tools: {}, }; +const RESOURCE_CAPABILITIES: Record = { + 'finance://portfolio/summary': 'finance.mcp.resources.read:portfolio-summary', + 'finance://properties': 'finance.mcp.resources.read:properties', + 'finance://tenants': 'finance.mcp.resources.read:tenants', +}; + const RESOURCES = [ { uri: 'finance://portfolio/summary', @@ -289,7 +295,7 @@ mcpRoutes.post('/mcp', async (c) => { case 'resources/read': { const uri = body.params?.uri; if (!uri) return c.json(rpcError(body.id, -32602, 'Missing uri param'), 400); - setExecutionOperation(c, `finance.mcp.resources.read:${uri}`, 'read'); + setExecutionOperation(c, RESOURCE_CAPABILITIES[uri] ?? 'finance.mcp.resources.read:unknown', 'read'); const result = await readResource(uri, storage, tenantId, userId); return c.json(rpcOk(body.id, result)); } @@ -303,8 +309,11 @@ mcpRoutes.post('/mcp', async (c) => { const toolArgs = body.params?.arguments || {}; if (!toolName) return c.json(rpcError(body.id, -32602, 'Missing tool name'), 400); const toolDefinition = TOOL_DEFINITIONS.find((tool) => tool.name === toolName); - if (!toolDefinition) return c.json(rpcError(body.id, -32602, `Unknown tool: ${toolName}`), 400); - setExecutionOperation(c, `finance.mcp.tool:${toolName}`, toolDefinition.intent); + setExecutionOperation( + c, + toolDefinition ? `finance.mcp.tool:${toolName}` : 'finance.mcp.tools.call', + toolDefinition?.intent ?? 'execute', + ); const result = await callTool(toolName, toolArgs, storage, tenantId, c.env); return c.json(rpcOk(body.id, result)); } From 82970ebc25309af3ea548a2420e657e2edd4bcaf Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:27:56 -0500 Subject: [PATCH 18/21] test(mcp): verify real execution capability wiring --- server/__tests__/mcp.test.ts | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/server/__tests__/mcp.test.ts b/server/__tests__/mcp.test.ts index 18ddde2..c862c68 100644 --- a/server/__tests__/mcp.test.ts +++ b/server/__tests__/mcp.test.ts @@ -36,17 +36,28 @@ function createMockStorage() { function buildApp() { const app = new Hono(); const storage = createMockStorage(); + let observedExecution: any; // Inject mock storage + authorized caller scope into context app.use('*', async (c, next) => { c.set('storage', storage as any); c.set('tenantId', 'test-tenant'); c.set('userId', 'user-1'); + c.set('authMethod', 'chittyauth'); + c.set('executionContext', { + actor: { userId: 'user-1', authMethod: 'chittyauth' }, + source: { service: 'test', claimed: false }, + scope: { tenantId: 'test-tenant' }, + capability: 'finance.http.post:/mcp', + intent: 'execute', + trace: { requestId: 'test-request' }, + }); await next(); + observedExecution = c.get('executionContext'); }); app.route('/', mcpRoutes); - return { app, storage }; + return { app, storage, getExecution: () => observedExecution }; } function rpc(app: Hono, method: string, params?: Record, id: number | string = 1) { @@ -60,9 +71,10 @@ function rpc(app: Hono, method: string, params?: Record, i describe('MCP endpoint', () => { let app: Hono; let storage: ReturnType; + let getExecution: () => any; beforeEach(() => { - ({ app, storage } = buildApp()); + ({ app, storage, getExecution } = buildApp()); }); // ── Protocol ── @@ -76,6 +88,8 @@ describe('MCP endpoint', () => { expect(body.result.serverInfo.name).toBe('chittyfinance'); expect(body.result.capabilities.resources).toBeDefined(); expect(body.result.capabilities.tools).toBeDefined(); + expect(getExecution().capability).toBe('finance.mcp.initialize'); + expect(getExecution().intent).toBe('read'); }); it('rejects bad JSON', async () => { @@ -128,6 +142,8 @@ describe('MCP endpoint', () => { expect(data.totalProperties).toBe(2); expect(data.totalValue).toBe(600000); expect(data.totalNOI).toBe(30000); // 15000 * 2 + expect(getExecution().capability).toBe('finance.mcp.resources.read:portfolio-summary'); + expect(getExecution().intent).toBe('read'); }); it('reads finance://properties', async () => { @@ -197,6 +213,8 @@ describe('MCP endpoint', () => { expect(body.result.content).toHaveLength(1); expect(body.result.content[0].text).toContain('City Studio'); expect(body.result.content[0].text).toContain('Rule-based advice'); + expect(getExecution().capability).toBe('finance.mcp.tool:get-property-advice'); + expect(getExecution().intent).toBe('suggest'); }); it('calls refresh-valuation', async () => { @@ -207,6 +225,8 @@ describe('MCP endpoint', () => { const body = await res.json() as any; expect(body.result.content[0].text).toContain('Valuation refresh queued'); expect(body.result.content[0].text).toContain('City Studio'); + expect(getExecution().capability).toBe('finance.mcp.tool:refresh-valuation'); + expect(getExecution().intent).toBe('execute'); }); it('returns not-found for missing property in tool call', async () => { @@ -219,14 +239,16 @@ describe('MCP endpoint', () => { expect(body.result.content[0].text).toContain('not found'); }); - it('rejects unknown tool before execution', async () => { + it('preserves the existing unknown-tool error path', async () => { const res = await rpc(app, 'tools/call', { name: 'nonexistent-tool', arguments: {}, }); - expect(res.status).toBe(400); + expect(res.status).toBe(500); const body = await res.json() as any; - expect(body.error.code).toBe(-32602); + expect(body.error.code).toBe(-32000); + expect(getExecution().capability).toBe('finance.mcp.tools.call'); + expect(getExecution().intent).toBe('execute'); }); it('returns error for missing tool name', async () => { From a749d279c79655c4de1ba495f9c8dc35d8f0856d Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:28:06 -0500 Subject: [PATCH 19/21] test(audit): verify ledger execution provenance enrichment --- server/__tests__/ledger-client.test.ts | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/server/__tests__/ledger-client.test.ts b/server/__tests__/ledger-client.test.ts index 1d02b42..1d31437 100644 --- a/server/__tests__/ledger-client.test.ts +++ b/server/__tests__/ledger-client.test.ts @@ -2,7 +2,7 @@ * Ledger client tests — mocked fetch, no real network calls */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { postLedgerEntry, resolveLedgerBase, logToLedger } from '../lib/ledger-client'; +import { postLedgerEntry, resolveLedgerBase, logToLedger, ledgerLog } from '../lib/ledger-client'; const MOCK_RESPONSE = { id: 'uuid-1', sequenceNumber: '42', hash: 'abc123def456' }; @@ -107,4 +107,38 @@ describe('ledger-client', () => { )).resolves.toBeUndefined(); }); }); + + describe('ledgerLog', () => { + it('attaches execution provenance while preserving existing metadata', async () => { + fetchSpy.mockResolvedValue(new Response(JSON.stringify(MOCK_RESPONSE), { status: 200 })); + + let pending: Promise | undefined; + const execution = { + actor: { userId: 'user-1', authMethod: 'chittyauth' as const }, + source: { service: 'chittyclaw', claimed: true, channel: 'slack' }, + scope: { tenantId: 'tenant-1' }, + capability: 'finance.allocations.execute', + intent: 'execute' as const, + trace: { requestId: 'req-1' }, + }; + + ledgerLog({ + executionCtx: { waitUntil: (promise) => { pending = promise; } }, + get: () => execution, + }, { + entityType: 'audit', + action: 'allocation.executed', + metadata: { period: '2026-09' }, + }, { + CHITTY_LEDGER_BASE: 'https://ledger.chitty.cc', + CHITTY_AUTH_SERVICE_TOKEN: 'tok-123', + }); + + await pending; + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body); + expect(body.metadata.period).toBe('2026-09'); + expect(body.metadata.execution).toEqual(execution); + }); + }); }); From 8007be668175231c7275b0e3e8caeed884a59e58 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:28:17 -0500 Subject: [PATCH 20/21] docs(security): mark provenance and intent non-authoritative --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 4524462..6f1ac9b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -59,9 +59,9 @@ We follow coordinated disclosure and will credit reporters unless anonymity is p ### Cross-Channel Provenance -- Existing `X-Source-Service` identifies the calling ChittyOS/platform adapter for audit purposes +- Existing `X-Source-Service` is recorded as a caller-claimed adapter identity for audit correlation; it is not an attestation - W3C `traceparent` carries distributed trace linkage; W3C `baggage` may carry `chitty.source`, `chitty.channel`, `chitty.workspace`, and `chitty.session` -- Provenance fields are untrusted authorization inputs: they are never used to derive `userId`, tenant membership, or financial role +- Provenance fields and inferred `intent` are non-authoritative audit context: they are never used to derive `userId`, tenant membership, financial role, or write permission - ChittyLedger audit writes inherit the request execution context automatically ### OAuth & Webhook Security From b21d4c478e241290fc8c8896b575e2df9f7cf124 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:28:19 -0500 Subject: [PATCH 21/21] docs(agents): clarify audit-only execution intent --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 72d382b..0f8280b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ When working in this repo, prefer these subagents (see user's `~/.claude/agents/ ### Channel-Neutral Execution Context -Protected requests are normalized after authentication + tenant membership checks. The runtime context is request-scoped only (no new database/schema) and contains the verified actor, authorized tenant scope, inferred or route-specific capability/intent, source provenance, and trace metadata. `X-Source-Service` plus W3C `traceparent`/`baggage` carry portable provenance for ChatGPT, Claude, ChittyClaw/OpenClaw, and other adapters. Platform workspace/channel metadata is audit context only and MUST NOT affect `tenant_users` authorization. +Protected requests are normalized after authentication + tenant membership checks. The runtime context is request-scoped only (no new database/schema) and contains the verified actor, authorized tenant scope, inferred or route-specific capability/intent, source provenance, and trace metadata. `X-Source-Service` plus W3C `traceparent`/`baggage` carry portable provenance for ChatGPT, Claude, ChittyClaw/OpenClaw, and other adapters. Platform workspace/channel metadata and inferred intent are audit context only and MUST NOT affect `tenant_users` authorization or mutation authority. **ChittyFinance does delegate these to external agents:** - Identity (ChittyID via OAuth 2.0 PKCE)