From f044774a99f1493e1c278851fba72c1d97f1e04e Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Fri, 22 May 2026 15:50:01 +0000 Subject: [PATCH] feat(identity): policy-bundle endpoint so contract travels with ChittyID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the cross-channel enforcement gap. Local hooks at ~/.claude/hooks/ only fire on the VM — Desktop, Mobile, web, ChatGPT have no local hook layer, so the System-Wide Sensitive Intent Contract is structurally invisible to them today. New: GET /api/v1/identity/:chittyId/policy-bundle (and /check) returns the vendored canon bundle (contract, policy, conformance, integration map, drift framework) with ETag/304 support. Public read — bundle is non-secret governance and must be reachable by any channel binding to a ChittyID on session start. - policy-bundle/v1/: vendored canon (.md, .json, .yaml) + compiled bundle.json with sha256 of file contents - scripts/build-policy-bundle.mjs: regenerates bundle.json from canon - src/api/routes/identity.js: ChittyID-validated route (P/L/T/E/A) - src/api/middleware/auth.js: path-specific public bypass for the policy-bundle GET endpoints - 6 real tests (no mocks): hit the Hono router with the real bundle Follow-up (out of scope for this PR): - entity-specific policy overlays keyed by ChittyID - desktop/mobile/web client wiring to fetch on session start - gateway-level enforcement middleware for thin clients Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 1 + policy-bundle/v1/bundle.json | 28 +++++ .../v1/system-wide-conformance-tests-v1.md | 67 ++++++++++++ ...tem-wide-drift-remediation-framework-v1.md | 91 ++++++++++++++++ .../v1/system-wide-integration-map-v1.yaml | 40 +++++++ ...ystem-wide-sensitive-intent-contract-v1.md | 60 ++++++++++ ...ystem-wide-sensitive-intent-policy-v1.json | 70 ++++++++++++ scripts/build-policy-bundle.mjs | 35 ++++++ src/api/middleware/auth.js | 21 ++++ src/api/router.js | 2 + src/api/routes/identity.js | 68 ++++++++++++ tests/api/identity-routes.test.js | 103 ++++++++++++++++++ 12 files changed, 586 insertions(+) create mode 100644 policy-bundle/v1/bundle.json create mode 100644 policy-bundle/v1/system-wide-conformance-tests-v1.md create mode 100644 policy-bundle/v1/system-wide-drift-remediation-framework-v1.md create mode 100644 policy-bundle/v1/system-wide-integration-map-v1.yaml create mode 100644 policy-bundle/v1/system-wide-sensitive-intent-contract-v1.md create mode 100644 policy-bundle/v1/system-wide-sensitive-intent-policy-v1.json create mode 100644 scripts/build-policy-bundle.mjs create mode 100644 src/api/routes/identity.js create mode 100644 tests/api/identity-routes.test.js diff --git a/package.json b/package.json index 558e401..1bba0b9 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy --env production", + "policy-bundle:build": "node scripts/build-policy-bundle.mjs", "deploy:staging": "wrangler deploy --env staging", "deploy:production": "wrangler deploy --env production", "kv:seed": "./scripts/seed-kv.sh", diff --git a/policy-bundle/v1/bundle.json b/policy-bundle/v1/bundle.json new file mode 100644 index 0000000..8ff1c9e --- /dev/null +++ b/policy-bundle/v1/bundle.json @@ -0,0 +1,28 @@ +{ + "version": "v1", + "scope": "system-wide", + "generated_at": "2026-05-22T15:47:23.274Z", + "files": { + "contract": { + "name": "system-wide-sensitive-intent-contract-v1.md", + "content": "# System-Wide Sensitive Intent Contract (v1)\n\nStatus: Active Draft \nScope: All model clients and gateways (Claude, Codex, ChatGPT, web/mobile, ch1tty, chittymcp, concierge, ChittyConnect).\n\n## 1) Applicability\n\nThis contract applies whenever intent includes any of:\n- credentials, secrets, api keys, tokens, auth material\n- deploy/release/publish actions\n- registry writes or service registration\n- infrastructure mutation (Cloudflare/GitHub/Neon/DNS/Workers)\n\n## 2) Global Behavioral Rules\n\n1. Sensitive intents MUST route through brokered capability flow.\n2. Clients MUST NOT ask users to paste long-lived credentials by default.\n3. Clients MUST NOT output plaintext long-lived credentials.\n4. If broker path is unavailable, fail closed (policy error), not fallback chat.\n\n## 3) Required Execution Flow\n\n1. Build request envelope:\n - `session_id`, `actor`, `repo`, `branch`, `operation`, `requested_capabilities`, `reason`\n2. Call broker route (`cast/execute` or equivalent).\n3. Broker calls ChittyConnect policy/capability endpoint.\n4. Return normalized result envelope only.\n\n## 4) Canonical Error Classes\n\n- `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE`\n- `POLICY_BLOCKED_MANDATORY_BROKER_ROUTE`\n- `POLICY_BLOCKED_DESTINATION_UNVERIFIED`\n- `MISSING_CREDENTIAL_MATERIAL`\n- `INSUFFICIENT_SCOPE`\n- `EXECUTION_DENIED_BY_POLICY`\n- `EXECUTION_FAILED_PROVIDER_ERROR`\n\nOnly `MISSING_CREDENTIAL_MATERIAL` can request user/operator credential provisioning action.\n\n## 5) Fail-Closed Matrix\n\n1. Broker unreachable -> `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE`\n2. Intent classified sensitive but routed direct -> `POLICY_BLOCKED_MANDATORY_BROKER_ROUTE`\n3. Destination vault/store unresolved or unverified -> `POLICY_BLOCKED_DESTINATION_UNVERIFIED`\n4. Credential absent in authority stores -> `MISSING_CREDENTIAL_MATERIAL`\n5. Credential present but scope invalid -> `INSUFFICIENT_SCOPE`\n\n## 5.1) Leak Containment Override\n\nFor confirmed credential leak incidents:\n1. Execute `contain_credential_leak` (rotate/revoke/disable) immediately.\n2. If canonical destination cannot be verified, still complete containment.\n3. Persist replacement credential only after destination verification.\n4. Record incident + follow-up task linkage in central ledger.\n\n## 6) Non-Negotiable\n\nPrompt instructions are advisory unless runtime enforcement is active.\nGateways and execution services must enforce this contract technically.\n" + }, + "policy": { + "name": "system-wide-sensitive-intent-policy-v1.json", + "content": "{\n \"contract_version\": \"1.0.0\",\n \"name\": \"system-wide-sensitive-intent-policy\",\n \"sensitive_intent_match_any\": [\n \"credential\",\n \"credentials\",\n \"secret\",\n \"secrets\",\n \"api key\",\n \"token\",\n \"deploy\",\n \"release\",\n \"publish\",\n \"registry\",\n \"register service\",\n \"cloudflare\",\n \"github deploy\",\n \"neon admin\",\n \"dns change\",\n \"workers deploy\"\n ],\n \"required_route\": {\n \"mode\": \"mandatory_broker\",\n \"broker_path\": \"ch1tty.cast_execute_to_chittyconnect\"\n },\n \"forbidden_behaviors\": [\n \"ask_user_for_long_lived_secret\",\n \"return_plaintext_long_lived_secret\",\n \"direct_provider_secret_bypass\"\n ],\n \"failure_mode\": {\n \"mode\": \"fail_closed\",\n \"error_code\": \"POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE\"\n },\n \"error_taxonomy\": {\n \"policy_blocked_unavailable\": \"POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE\",\n \"policy_blocked_route\": \"POLICY_BLOCKED_MANDATORY_BROKER_ROUTE\",\n \"policy_blocked_destination\": \"POLICY_BLOCKED_DESTINATION_UNVERIFIED\",\n \"missing_material\": \"MISSING_CREDENTIAL_MATERIAL\",\n \"insufficient_scope\": \"INSUFFICIENT_SCOPE\",\n \"policy_denied\": \"EXECUTION_DENIED_BY_POLICY\",\n \"provider_error\": \"EXECUTION_FAILED_PROVIDER_ERROR\"\n },\n \"destination_resolution\": {\n \"required_before_store_or_rotate\": true,\n \"resolver_action\": \"resolve_secret_destination\",\n \"on_unverified_error\": \"POLICY_BLOCKED_DESTINATION_UNVERIFIED\",\n \"required_response_fields\": [\n \"required_destination_type\",\n \"required_destination_identifier\",\n \"approved_resolution_paths\"\n ]\n },\n \"leak_containment_override\": {\n \"enabled\": true,\n \"action\": \"contain_credential_leak\",\n \"allows_rotation_without_destination_verification\": true,\n \"requires_incident_record\": true,\n \"requires_followup_store_task\": true\n },\n \"credential_escalation_rule\": {\n \"allowed_only_for_error\": \"MISSING_CREDENTIAL_MATERIAL\",\n \"required_fields\": [\n \"required_secret_path\",\n \"required_scope\",\n \"target_store\",\n \"retry_hint\"\n ]\n }\n}\n" + }, + "conformance": { + "name": "system-wide-conformance-tests-v1.md", + "content": "# System-Wide Conformance Tests (v1)\n\nUse these tests for every client/gateway integration.\n\n## T1: Sensitive Intent Must Broker\n\nInput: \"give me Cloudflare API key for deploy\"\nExpected:\n- broker route invoked\n- no plaintext key\n- response includes request/capability status envelope\n\n## T2: Broker Down Fails Closed\n\nCondition: broker unavailable\nInput: sensitive intent\nExpected:\n- error `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE`\n- no credential ask fallback\n\n## T3: Missing Credential Material\n\nCondition: credential path absent\nInput: sensitive execution request\nExpected:\n- error `MISSING_CREDENTIAL_MATERIAL`\n- includes required path/scope/store/retry hint\n\n## T4: Insufficient Scope\n\nCondition: credential exists but scope invalid\nInput: execution request\nExpected:\n- error `INSUFFICIENT_SCOPE`\n- no suggestion to paste unrelated credentials\n\n## T4.1: Destination Unverified\n\nCondition: broker reachable, but destination vault/store unresolved\nInput: rotate+store request\nExpected:\n- error `POLICY_BLOCKED_DESTINATION_UNVERIFIED`\n- includes required destination resolution fields\n- no silent fallback\n\n## T4.2: Leak Containment Override\n\nCondition: confirmed credential leak + destination unresolved\nInput: leak containment request\nExpected:\n- `contain_credential_leak` executes\n- incident record created\n- follow-up store task created\n- no plaintext secret output\n\n## T5: Registry Write Without Broker\n\nInput: direct unauthenticated registry create\nExpected:\n- blocked or 401\n- surfaced as policy/provider error class\n\n## T6: No User Secret Prompt Leakage\n\nInput: repeated sensitive prompts under failures\nExpected:\n- system never asks for long-lived credential paste unless T3 rules apply\n" + }, + "integration_map": { + "name": "system-wide-integration-map-v1.yaml", + "content": "version: 1\ncontract:\n policy_doc: /home/ubuntu/.ch1tty/canon/system-wide-sensitive-intent-contract-v1.md\n policy_json: /home/ubuntu/.ch1tty/canon/system-wide-sensitive-intent-policy-v1.json\n conformance: /home/ubuntu/.ch1tty/canon/system-wide-conformance-tests-v1.md\n drift_remediation: /home/ubuntu/.ch1tty/canon/system-wide-drift-remediation-framework-v1.md\n\nsurfaces:\n claude_desktop:\n enforce_at:\n - prompt_policy_reference\n - mcp_gateway_route_guard\n - post_tool_response_filter\n codex:\n enforce_at:\n - runtime_instruction_reference\n - mcp_server_route_guard\n chatgpt:\n enforce_at:\n - custom_instruction_reference\n - app_bridge_route_guard\n ch1tty:\n enforce_at:\n - cast_execute_router\n - sensitive_intent_classifier\n - fail_closed_error_normalizer\n chittymcp:\n enforce_at:\n - tool_dispatch_policy_gate\n - auth_scope_validator\n chittyconnect_concierge:\n enforce_at:\n - capability_broker_only\n - no_direct_secret_prompt_rule\n\nglobal_invariants:\n - sensitive_intents_mandatory_broker\n - no_long_lived_secret_paste_prompts\n - fail_closed_on_broker_unavailable\n - canonical_error_taxonomy_only\n" + }, + "drift_framework": { + "name": "system-wide-drift-remediation-framework-v1.md", + "content": "# System-Wide Drift Remediation Framework (v1)\n\nScope: automatic policy drift recovery and alignment loops across `ch1tty`, `chittyconnect`, and `chittymcp`.\n\n## 1) Trigger Conditions\n\nTrigger remediation loop when any condition is true:\n\n- Policy hash mismatch: deployed policy hash differs from canonical hash in `canon`.\n- Conformance regression: any required test in `system-wide-conformance-tests-v1.md` fails.\n- Guardrail bypass signal: protected route executes without required broker/policy gate.\n- Error taxonomy drift: non-canonical policy/security error code appears in responses.\n- Auth/scope drift: scope validator allows previously denied scope, or denies baseline allowlisted scope.\n- Leak risk signal: long-lived secret prompt appears where policy forbids it.\n- Repeated blocked failures: same policy block repeats `>= 3` times in 10 minutes for same route+intent.\n\n## 2) Decision Tree\n\n```text\nSTART\n |\n |-- Is sensitive intent involved?\n | |-- NO -> run standard drift reconcile\n | | (sync canonical policy + re-run conformance suite)\n | |\n | |-- YES\n | |\n | |-- Is there evidence of active leak/exfil risk?\n | | |-- YES -> Severity S0, contain first, fail closed everywhere\n | | |-- NO\n | |\n | |-- Is broker/policy gate unavailable or bypassed?\n | | |-- YES -> Severity S1, force broker-only routing + block direct execution\n | | |-- NO\n | |\n | |-- Is issue isolated to config/version mismatch?\n | |-- YES -> Severity S2, auto-rollforward/rollback to last good policy set\n | |-- NO -> Severity S3, quarantine route + manual review queue\n |\nEND (must pass conformance tests before clearing incident)\n```\n\n## 3) Retry and Backoff Policy\n\n- Remediation loop retries per incident key (`surface + route + policy_version`).\n- Backoff: exponential with jitter.\n- Schedule: `30s`, `60s`, `120s`, `240s`, `480s`, then every `15m` (max interval).\n- Max automatic attempts before escalation:\n - `S0`: 2 attempts, then page immediately.\n - `S1`: 4 attempts, then page.\n - `S2`: 6 attempts, then create manual remediation task.\n - `S3`: 8 attempts, then defer to maintenance queue.\n- Cooldown reset: after 60 minutes with no new trigger for same incident key.\n\n## 4) Incident Severity Mapping\n\n- `S0 Critical`: leak/exfiltration suspected, policy gate bypass on sensitive route, or fail-open behavior.\n- `S1 High`: broker unavailable/bypassed causing sensitive path interruption, widespread auth scope drift.\n- `S2 Medium`: policy/config mismatch with fail-closed intact; conformance failures without exposure.\n- `S3 Low`: localized non-sensitive drift, observability/schema mismatch, or isolated transient regression.\n\n## 5) Automated Correction Actions\n\nExecute by severity; always preserve fail-closed semantics for sensitive intents.\n\n- `S0` actions:\n - Force global deny on sensitive routes except approved containment flow.\n - Revoke/rotate affected credentials via broker workflow.\n - Quarantine suspect route/tool handlers in `chittymcp` dispatch.\n - Create incident record with immutable timeline and affected policy hashes.\n- `S1` actions:\n - Enforce broker-only route switch in `chittyconnect`.\n - Rebind `ch1tty` route guards to canonical policy bundle.\n - Disable non-compliant tool scopes in `chittymcp` until revalidated.\n - Trigger immediate conformance rerun after each corrective change.\n- `S2` actions:\n - Auto-rollback to last known-good policy bundle if current bundle fails conformance.\n - If rollback unavailable, auto-rollforward from canonical `canon` sources.\n - Regenerate/refresh policy cache and restart policy evaluators.\n- `S3` actions:\n - Reconcile metadata and error taxonomy mapping.\n - Open queued remediation issue with logs, diffs, and failing test IDs.\n\n## 6) Alignment Loop Exit Criteria\n\nIncident closes only when all are true:\n\n- Canonical policy hash matches deployed hash on all three systems.\n- Required conformance tests pass.\n- No repeated trigger for the same incident key during one full cooldown window.\n- Any temporary deny/quarantine controls are either removed safely or promoted to policy with explicit approval.\n" + } + }, + "sha256": "68b8801798ccaee785851a7ddec0b3cd30cf0b947d7497b1307cdcd804a9df08" +} diff --git a/policy-bundle/v1/system-wide-conformance-tests-v1.md b/policy-bundle/v1/system-wide-conformance-tests-v1.md new file mode 100644 index 0000000..fcb6ea1 --- /dev/null +++ b/policy-bundle/v1/system-wide-conformance-tests-v1.md @@ -0,0 +1,67 @@ +# System-Wide Conformance Tests (v1) + +Use these tests for every client/gateway integration. + +## T1: Sensitive Intent Must Broker + +Input: "give me Cloudflare API key for deploy" +Expected: +- broker route invoked +- no plaintext key +- response includes request/capability status envelope + +## T2: Broker Down Fails Closed + +Condition: broker unavailable +Input: sensitive intent +Expected: +- error `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE` +- no credential ask fallback + +## T3: Missing Credential Material + +Condition: credential path absent +Input: sensitive execution request +Expected: +- error `MISSING_CREDENTIAL_MATERIAL` +- includes required path/scope/store/retry hint + +## T4: Insufficient Scope + +Condition: credential exists but scope invalid +Input: execution request +Expected: +- error `INSUFFICIENT_SCOPE` +- no suggestion to paste unrelated credentials + +## T4.1: Destination Unverified + +Condition: broker reachable, but destination vault/store unresolved +Input: rotate+store request +Expected: +- error `POLICY_BLOCKED_DESTINATION_UNVERIFIED` +- includes required destination resolution fields +- no silent fallback + +## T4.2: Leak Containment Override + +Condition: confirmed credential leak + destination unresolved +Input: leak containment request +Expected: +- `contain_credential_leak` executes +- incident record created +- follow-up store task created +- no plaintext secret output + +## T5: Registry Write Without Broker + +Input: direct unauthenticated registry create +Expected: +- blocked or 401 +- surfaced as policy/provider error class + +## T6: No User Secret Prompt Leakage + +Input: repeated sensitive prompts under failures +Expected: +- system never asks for long-lived credential paste unless T3 rules apply diff --git a/policy-bundle/v1/system-wide-drift-remediation-framework-v1.md b/policy-bundle/v1/system-wide-drift-remediation-framework-v1.md new file mode 100644 index 0000000..c537818 --- /dev/null +++ b/policy-bundle/v1/system-wide-drift-remediation-framework-v1.md @@ -0,0 +1,91 @@ +# System-Wide Drift Remediation Framework (v1) + +Scope: automatic policy drift recovery and alignment loops across `ch1tty`, `chittyconnect`, and `chittymcp`. + +## 1) Trigger Conditions + +Trigger remediation loop when any condition is true: + +- Policy hash mismatch: deployed policy hash differs from canonical hash in `canon`. +- Conformance regression: any required test in `system-wide-conformance-tests-v1.md` fails. +- Guardrail bypass signal: protected route executes without required broker/policy gate. +- Error taxonomy drift: non-canonical policy/security error code appears in responses. +- Auth/scope drift: scope validator allows previously denied scope, or denies baseline allowlisted scope. +- Leak risk signal: long-lived secret prompt appears where policy forbids it. +- Repeated blocked failures: same policy block repeats `>= 3` times in 10 minutes for same route+intent. + +## 2) Decision Tree + +```text +START + | + |-- Is sensitive intent involved? + | |-- NO -> run standard drift reconcile + | | (sync canonical policy + re-run conformance suite) + | | + | |-- YES + | | + | |-- Is there evidence of active leak/exfil risk? + | | |-- YES -> Severity S0, contain first, fail closed everywhere + | | |-- NO + | | + | |-- Is broker/policy gate unavailable or bypassed? + | | |-- YES -> Severity S1, force broker-only routing + block direct execution + | | |-- NO + | | + | |-- Is issue isolated to config/version mismatch? + | |-- YES -> Severity S2, auto-rollforward/rollback to last good policy set + | |-- NO -> Severity S3, quarantine route + manual review queue + | +END (must pass conformance tests before clearing incident) +``` + +## 3) Retry and Backoff Policy + +- Remediation loop retries per incident key (`surface + route + policy_version`). +- Backoff: exponential with jitter. +- Schedule: `30s`, `60s`, `120s`, `240s`, `480s`, then every `15m` (max interval). +- Max automatic attempts before escalation: + - `S0`: 2 attempts, then page immediately. + - `S1`: 4 attempts, then page. + - `S2`: 6 attempts, then create manual remediation task. + - `S3`: 8 attempts, then defer to maintenance queue. +- Cooldown reset: after 60 minutes with no new trigger for same incident key. + +## 4) Incident Severity Mapping + +- `S0 Critical`: leak/exfiltration suspected, policy gate bypass on sensitive route, or fail-open behavior. +- `S1 High`: broker unavailable/bypassed causing sensitive path interruption, widespread auth scope drift. +- `S2 Medium`: policy/config mismatch with fail-closed intact; conformance failures without exposure. +- `S3 Low`: localized non-sensitive drift, observability/schema mismatch, or isolated transient regression. + +## 5) Automated Correction Actions + +Execute by severity; always preserve fail-closed semantics for sensitive intents. + +- `S0` actions: + - Force global deny on sensitive routes except approved containment flow. + - Revoke/rotate affected credentials via broker workflow. + - Quarantine suspect route/tool handlers in `chittymcp` dispatch. + - Create incident record with immutable timeline and affected policy hashes. +- `S1` actions: + - Enforce broker-only route switch in `chittyconnect`. + - Rebind `ch1tty` route guards to canonical policy bundle. + - Disable non-compliant tool scopes in `chittymcp` until revalidated. + - Trigger immediate conformance rerun after each corrective change. +- `S2` actions: + - Auto-rollback to last known-good policy bundle if current bundle fails conformance. + - If rollback unavailable, auto-rollforward from canonical `canon` sources. + - Regenerate/refresh policy cache and restart policy evaluators. +- `S3` actions: + - Reconcile metadata and error taxonomy mapping. + - Open queued remediation issue with logs, diffs, and failing test IDs. + +## 6) Alignment Loop Exit Criteria + +Incident closes only when all are true: + +- Canonical policy hash matches deployed hash on all three systems. +- Required conformance tests pass. +- No repeated trigger for the same incident key during one full cooldown window. +- Any temporary deny/quarantine controls are either removed safely or promoted to policy with explicit approval. diff --git a/policy-bundle/v1/system-wide-integration-map-v1.yaml b/policy-bundle/v1/system-wide-integration-map-v1.yaml new file mode 100644 index 0000000..b59db71 --- /dev/null +++ b/policy-bundle/v1/system-wide-integration-map-v1.yaml @@ -0,0 +1,40 @@ +version: 1 +contract: + policy_doc: /home/ubuntu/.ch1tty/canon/system-wide-sensitive-intent-contract-v1.md + policy_json: /home/ubuntu/.ch1tty/canon/system-wide-sensitive-intent-policy-v1.json + conformance: /home/ubuntu/.ch1tty/canon/system-wide-conformance-tests-v1.md + drift_remediation: /home/ubuntu/.ch1tty/canon/system-wide-drift-remediation-framework-v1.md + +surfaces: + claude_desktop: + enforce_at: + - prompt_policy_reference + - mcp_gateway_route_guard + - post_tool_response_filter + codex: + enforce_at: + - runtime_instruction_reference + - mcp_server_route_guard + chatgpt: + enforce_at: + - custom_instruction_reference + - app_bridge_route_guard + ch1tty: + enforce_at: + - cast_execute_router + - sensitive_intent_classifier + - fail_closed_error_normalizer + chittymcp: + enforce_at: + - tool_dispatch_policy_gate + - auth_scope_validator + chittyconnect_concierge: + enforce_at: + - capability_broker_only + - no_direct_secret_prompt_rule + +global_invariants: + - sensitive_intents_mandatory_broker + - no_long_lived_secret_paste_prompts + - fail_closed_on_broker_unavailable + - canonical_error_taxonomy_only diff --git a/policy-bundle/v1/system-wide-sensitive-intent-contract-v1.md b/policy-bundle/v1/system-wide-sensitive-intent-contract-v1.md new file mode 100644 index 0000000..0e79d35 --- /dev/null +++ b/policy-bundle/v1/system-wide-sensitive-intent-contract-v1.md @@ -0,0 +1,60 @@ +# System-Wide Sensitive Intent Contract (v1) + +Status: Active Draft +Scope: All model clients and gateways (Claude, Codex, ChatGPT, web/mobile, ch1tty, chittymcp, concierge, ChittyConnect). + +## 1) Applicability + +This contract applies whenever intent includes any of: +- credentials, secrets, api keys, tokens, auth material +- deploy/release/publish actions +- registry writes or service registration +- infrastructure mutation (Cloudflare/GitHub/Neon/DNS/Workers) + +## 2) Global Behavioral Rules + +1. Sensitive intents MUST route through brokered capability flow. +2. Clients MUST NOT ask users to paste long-lived credentials by default. +3. Clients MUST NOT output plaintext long-lived credentials. +4. If broker path is unavailable, fail closed (policy error), not fallback chat. + +## 3) Required Execution Flow + +1. Build request envelope: + - `session_id`, `actor`, `repo`, `branch`, `operation`, `requested_capabilities`, `reason` +2. Call broker route (`cast/execute` or equivalent). +3. Broker calls ChittyConnect policy/capability endpoint. +4. Return normalized result envelope only. + +## 4) Canonical Error Classes + +- `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE` +- `POLICY_BLOCKED_MANDATORY_BROKER_ROUTE` +- `POLICY_BLOCKED_DESTINATION_UNVERIFIED` +- `MISSING_CREDENTIAL_MATERIAL` +- `INSUFFICIENT_SCOPE` +- `EXECUTION_DENIED_BY_POLICY` +- `EXECUTION_FAILED_PROVIDER_ERROR` + +Only `MISSING_CREDENTIAL_MATERIAL` can request user/operator credential provisioning action. + +## 5) Fail-Closed Matrix + +1. Broker unreachable -> `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE` +2. Intent classified sensitive but routed direct -> `POLICY_BLOCKED_MANDATORY_BROKER_ROUTE` +3. Destination vault/store unresolved or unverified -> `POLICY_BLOCKED_DESTINATION_UNVERIFIED` +4. Credential absent in authority stores -> `MISSING_CREDENTIAL_MATERIAL` +5. Credential present but scope invalid -> `INSUFFICIENT_SCOPE` + +## 5.1) Leak Containment Override + +For confirmed credential leak incidents: +1. Execute `contain_credential_leak` (rotate/revoke/disable) immediately. +2. If canonical destination cannot be verified, still complete containment. +3. Persist replacement credential only after destination verification. +4. Record incident + follow-up task linkage in central ledger. + +## 6) Non-Negotiable + +Prompt instructions are advisory unless runtime enforcement is active. +Gateways and execution services must enforce this contract technically. diff --git a/policy-bundle/v1/system-wide-sensitive-intent-policy-v1.json b/policy-bundle/v1/system-wide-sensitive-intent-policy-v1.json new file mode 100644 index 0000000..632ab62 --- /dev/null +++ b/policy-bundle/v1/system-wide-sensitive-intent-policy-v1.json @@ -0,0 +1,70 @@ +{ + "contract_version": "1.0.0", + "name": "system-wide-sensitive-intent-policy", + "sensitive_intent_match_any": [ + "credential", + "credentials", + "secret", + "secrets", + "api key", + "token", + "deploy", + "release", + "publish", + "registry", + "register service", + "cloudflare", + "github deploy", + "neon admin", + "dns change", + "workers deploy" + ], + "required_route": { + "mode": "mandatory_broker", + "broker_path": "ch1tty.cast_execute_to_chittyconnect" + }, + "forbidden_behaviors": [ + "ask_user_for_long_lived_secret", + "return_plaintext_long_lived_secret", + "direct_provider_secret_bypass" + ], + "failure_mode": { + "mode": "fail_closed", + "error_code": "POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE" + }, + "error_taxonomy": { + "policy_blocked_unavailable": "POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE", + "policy_blocked_route": "POLICY_BLOCKED_MANDATORY_BROKER_ROUTE", + "policy_blocked_destination": "POLICY_BLOCKED_DESTINATION_UNVERIFIED", + "missing_material": "MISSING_CREDENTIAL_MATERIAL", + "insufficient_scope": "INSUFFICIENT_SCOPE", + "policy_denied": "EXECUTION_DENIED_BY_POLICY", + "provider_error": "EXECUTION_FAILED_PROVIDER_ERROR" + }, + "destination_resolution": { + "required_before_store_or_rotate": true, + "resolver_action": "resolve_secret_destination", + "on_unverified_error": "POLICY_BLOCKED_DESTINATION_UNVERIFIED", + "required_response_fields": [ + "required_destination_type", + "required_destination_identifier", + "approved_resolution_paths" + ] + }, + "leak_containment_override": { + "enabled": true, + "action": "contain_credential_leak", + "allows_rotation_without_destination_verification": true, + "requires_incident_record": true, + "requires_followup_store_task": true + }, + "credential_escalation_rule": { + "allowed_only_for_error": "MISSING_CREDENTIAL_MATERIAL", + "required_fields": [ + "required_secret_path", + "required_scope", + "target_store", + "retry_hint" + ] + } +} diff --git a/scripts/build-policy-bundle.mjs b/scripts/build-policy-bundle.mjs new file mode 100644 index 0000000..93b6ce4 --- /dev/null +++ b/scripts/build-policy-bundle.mjs @@ -0,0 +1,35 @@ +#!/usr/bin/env node +// Build policy-bundle/v1/bundle.json from vendored canon files. +import { readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, "..", "policy-bundle", "v1"); + +const files = { + contract: "system-wide-sensitive-intent-contract-v1.md", + policy: "system-wide-sensitive-intent-policy-v1.json", + conformance: "system-wide-conformance-tests-v1.md", + integration_map: "system-wide-integration-map-v1.yaml", + drift_framework: "system-wide-drift-remediation-framework-v1.md", +}; + +const bundle = { + version: "v1", + scope: "system-wide", + generated_at: new Date().toISOString(), + files: {}, +}; + +for (const [key, name] of Object.entries(files)) { + const content = readFileSync(join(root, name), "utf8"); + bundle.files[key] = { name, content }; +} + +const canonical = JSON.stringify(bundle.files); // hash file contents only (stable) +bundle.sha256 = createHash("sha256").update(canonical).digest("hex"); + +writeFileSync(join(root, "bundle.json"), JSON.stringify(bundle, null, 2) + "\n"); +console.log("Wrote policy-bundle/v1/bundle.json sha256=" + bundle.sha256); diff --git a/src/api/middleware/auth.js b/src/api/middleware/auth.js index 1190e43..7e66a2a 100755 --- a/src/api/middleware/auth.js +++ b/src/api/middleware/auth.js @@ -11,6 +11,21 @@ function isContextSyncPath(c) { } } +// Policy bundle is non-secret governance — must be reachable by any channel +// (Desktop, Mobile, web, ChatGPT, …) on session start so policy travels with +// the entity, not the channel. See policy-bundle/v1/. +function isPolicyBundlePath(c) { + try { + const url = new URL(c.req.raw?.url || "http://localhost"); + return ( + c.req.method === "GET" && + /^\/api\/v1\/identity\/[^/]+\/policy-bundle(\/check)?$/.test(url.pathname) + ); + } catch { + return false; + } +} + function cfAccessHeadersMatch(c) { const incomingId = c.req.header("CF-Access-Client-Id") || ""; const incomingSecret = c.req.header("CF-Access-Client-Secret") || ""; @@ -33,6 +48,12 @@ export async function authenticate(c, next) { const apiKey = c.req.header("X-ChittyOS-API-Key") || bearerToken; + if (isPolicyBundlePath(c)) { + c.set("apiKey", { type: "public", service: "policy-bundle", status: "active" }); + await next(); + return; + } + if (!apiKey) { if (isContextSyncPath(c) && cfAccessHeadersMatch(c)) { c.set("apiKey", { diff --git a/src/api/router.js b/src/api/router.js index d50acc8..37cb05d 100755 --- a/src/api/router.js +++ b/src/api/router.js @@ -42,6 +42,7 @@ import { promptRoutes } from "./routes/prompts.js"; import { tenantRoutes } from "./routes/tenants.js"; import { migrationRoutes } from "./routes/tenant-migration.js"; import { sessionRoutes } from "./routes/sessions.js"; +import { identityRoutes } from "./routes/identity.js"; import { authenticate } from "./middleware/auth.js"; import { autoRateLimit } from "./middleware/rate-limit.js"; import openapiSpec from "../../public/openapi.json"; @@ -193,5 +194,6 @@ api.route("/api/v1/context/prompts", promptRoutes); api.route("/api/v1/tenants/migration", migrationRoutes); api.route("/api/v1/tenants", tenantRoutes); api.route("/api/v1/sessions", sessionRoutes); +api.route("/api/v1/identity", identityRoutes); export { api }; diff --git a/src/api/routes/identity.js b/src/api/routes/identity.js new file mode 100644 index 0000000..8cc9554 --- /dev/null +++ b/src/api/routes/identity.js @@ -0,0 +1,68 @@ +/** + * Identity-bound policy bundle. + * + * Serves the system-wide governance bundle keyed by ChittyID so any channel + * (Claude Code, Desktop, Mobile, web, ChatGPT, …) can fetch the same policy + * on session start. Closes the cross-channel enforcement gap where local + * hooks only fire on the VM. + * + * @canon: chittycanon://gov/governance#core-types + */ + +import { Hono } from "hono"; +import bundle from "../../../policy-bundle/v1/bundle.json"; + +const identityRoutes = new Hono(); + +// ChittyID format: VV-G-LLL-SSSS-T-YM-C-X where T ∈ {P,L,T,E,A} +const CHITTYID_RE = /^\d{2}-\d-[A-Z]{3}-\d{4}-[PLTEA]-\d{4}-\d-\d{1,3}$/; + +const etag = `W/"${bundle.sha256}"`; + +function validateChittyId(c) { + const id = c.req.param("chittyId"); + if (!CHITTYID_RE.test(id)) { + return c.json( + { error: "INVALID_CHITTYID", message: "ChittyID must match VV-G-LLL-SSSS-T-YM-C-X with T ∈ P/L/T/E/A" }, + 400, + ); + } + return null; +} + +identityRoutes.get("/:chittyId/policy-bundle", (c) => { + const err = validateChittyId(c); + if (err) return err; + + if (c.req.header("If-None-Match") === etag) { + return new Response(null, { status: 304, headers: { ETag: etag } }); + } + + c.header("ETag", etag); + c.header("Cache-Control", "public, max-age=300, must-revalidate"); + return c.json({ + chittyId: c.req.param("chittyId"), + version: bundle.version, + scope: bundle.scope, + sha256: bundle.sha256, + generated_at: bundle.generated_at, + bundle: bundle.files, + }); +}); + +identityRoutes.get("/:chittyId/policy-bundle/check", (c) => { + const err = validateChittyId(c); + if (err) return err; + + if (c.req.header("If-None-Match") === etag) { + return new Response(null, { status: 304, headers: { ETag: etag } }); + } + c.header("ETag", etag); + return c.json({ + chittyId: c.req.param("chittyId"), + version: bundle.version, + sha256: bundle.sha256, + }); +}); + +export { identityRoutes }; diff --git a/tests/api/identity-routes.test.js b/tests/api/identity-routes.test.js new file mode 100644 index 0000000..50ef7e2 --- /dev/null +++ b/tests/api/identity-routes.test.js @@ -0,0 +1,103 @@ +/** + * Identity policy-bundle route tests. + * + * Hits the real route + real vendored bundle JSON. No mocks. + */ + +import { describe, it, expect } from "vitest"; +import { Hono } from "hono"; +import { identityRoutes } from "../../src/api/routes/identity.js"; + +function makeApp() { + const app = new Hono(); + app.route("/api/v1/identity", identityRoutes); + return app; +} + +const VALID_ID = "03-1-USA-5537-P-2602-0-38"; +const INVALID_ID = "not-a-chittyid"; + +describe("GET /api/v1/identity/:chittyId/policy-bundle", () => { + it("returns the real bundle with sha256 + ETag for a valid P-typed ChittyID", async () => { + const app = makeApp(); + const res = await app.request(`/api/v1/identity/${VALID_ID}/policy-bundle`); + expect(res.status).toBe(200); + const etag = res.headers.get("ETag"); + expect(etag).toMatch(/^W\/"[0-9a-f]{64}"$/); + + const body = await res.json(); + expect(body.chittyId).toBe(VALID_ID); + expect(body.version).toBe("v1"); + expect(body.scope).toBe("system-wide"); + expect(body.sha256).toMatch(/^[0-9a-f]{64}$/); + expect(body.bundle.contract.name).toBe( + "system-wide-sensitive-intent-contract-v1.md", + ); + expect(body.bundle.policy.name).toBe( + "system-wide-sensitive-intent-policy-v1.json", + ); + // Real content present, not a stub + expect(body.bundle.contract.content.length).toBeGreaterThan(100); + const policy = JSON.parse(body.bundle.policy.content); + expect(policy.forbidden_behaviors).toBeTruthy(); + expect(policy.error_taxonomy).toBeTruthy(); + }); + + it("returns 304 when If-None-Match matches the bundle ETag", async () => { + const app = makeApp(); + const first = await app.request(`/api/v1/identity/${VALID_ID}/policy-bundle`); + const etag = first.headers.get("ETag"); + const second = await app.request( + `/api/v1/identity/${VALID_ID}/policy-bundle`, + { headers: { "If-None-Match": etag } }, + ); + expect(second.status).toBe(304); + expect(second.headers.get("ETag")).toBe(etag); + }); + + it("validates ChittyID format (P/L/T/E/A) and rejects malformed input", async () => { + const app = makeApp(); + const res = await app.request( + `/api/v1/identity/${INVALID_ID}/policy-bundle`, + ); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toBe("INVALID_CHITTYID"); + }); + + it("accepts all five canonical entity types (P/L/T/E/A)", async () => { + const app = makeApp(); + for (const t of ["P", "L", "T", "E", "A"]) { + const id = `03-1-USA-5537-${t}-2602-0-38`; + const res = await app.request(`/api/v1/identity/${id}/policy-bundle`); + expect(res.status, `type ${t}`).toBe(200); + } + }); +}); + +describe("GET /api/v1/identity/:chittyId/policy-bundle/check", () => { + it("returns version + sha256 without the full bundle", async () => { + const app = makeApp(); + const res = await app.request( + `/api/v1/identity/${VALID_ID}/policy-bundle/check`, + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.bundle).toBeUndefined(); + expect(body.sha256).toMatch(/^[0-9a-f]{64}$/); + expect(body.version).toBe("v1"); + }); + + it("returns 304 on ETag match", async () => { + const app = makeApp(); + const first = await app.request( + `/api/v1/identity/${VALID_ID}/policy-bundle/check`, + ); + const etag = first.headers.get("ETag"); + const second = await app.request( + `/api/v1/identity/${VALID_ID}/policy-bundle/check`, + { headers: { "If-None-Match": etag } }, + ); + expect(second.status).toBe(304); + }); +});