diff --git a/canonical/contracts/credential-access.md b/canonical/contracts/credential-access.md new file mode 100644 index 0000000..11ce1f1 --- /dev/null +++ b/canonical/contracts/credential-access.md @@ -0,0 +1,70 @@ +--- +name: credential-access-contract +canon_uri: chittycanon://core/contracts/credential-access +version: 1.0.0 +status: proposed +extends: chittycanon://gov/governance#sensitive-intent # system-wide-sensitive-intent-contract-v1 +source_of_truth: chittyos-core/skills/nb-development-defaults/references/secrets.md +owner: chittyconnect-concierge (/chico) +applies_to: [worker, mcp-server, agent, chittyscript, plugin, skill, pentad-SECURITY] +--- + +# Credential Access Contract (v1) + +The single canonical contract for how ANY ChittyOS artifact touches credentials. +Templates and services **cite this URI**; they do not restate its prose. Restating +is what drifts (three templates independently rotted to "1Password"). One contract, +controlled projections, enforced in CI. + +## Invariants (non-negotiable) + +1. **Operator-zero-access.** The operator never retrieves, rotates, pastes, or + relays a secret. No secret value appears in code, config, logs, or chat — ever + (not from the user, not from tool output, not as an "example"). +2. **Resolution is a bound-service privilege.** A secret value is resolved ONLY + inside a bound service, via `getServiceToken(env, "")` or + `await env.SECRETS_STORE.get("")` (async, try/catch). A **consumer never + resolves a value**. +3. **Consumers carry one identity.** An off-service consumer presents only its own + `CHITTYCONNECT_SERVICE_TOKEN` (the inbound identity ChittyConnect validates). It + **must not** hold or present `CF_ACCESS_CLIENT_ID` / `CF_ACCESS_CLIENT_SECRET` — + those are the bound service's identity, and hand-authenticating to a gated host + with them is a blocked pattern. +4. **Route through the broker.** Any credential / deploy / registry-mutation intent + routes through ChittyConnect (`chittyconnect-concierge`, `/chico`). +5. **Fail closed.** Broker/route unavailable is a policy error, never a fallback: + `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE`, `POLICY_BLOCKED_MANDATORY_BROKER_ROUTE`, + `POLICY_BLOCKED_DESTINATION_UNVERIFIED`, `INSUFFICIENT_SCOPE`, + `EXECUTION_DENIED_BY_POLICY`. Only `MISSING_CREDENTIAL_MATERIAL` may request + provisioning (with full resolution fields). +6. **Storage + naming.** Tokens/keys/signing material live in the Cloudflare Secrets + Store (fronted by **ChittySecrets**, Layer 0). Never `[vars]`, never KV-as-truth. + Names: `CHITTYAUTH_ISSUED__` (legacy `CHITTY__TOKEN` + is fallback; `getServiceToken()` resolves both). Service URLs / DB IDs are `vars`, + not secrets. **1Password is RETIRED** — never a source of truth. + +## Roles + +| Role | May resolve a value? | Identity it presents | +|---|---|---| +| Bound service (Worker) | Yes — `getServiceToken` / `SECRETS_STORE.get` | its Secrets-Store binding / CF Access service token | +| Consumer (off-service: Apps Script, CLI, external) | **No** | one `CHITTYCONNECT_SERVICE_TOKEN` → bound egress route | +| Broker (`/chico`) | Yes — owns the lane | CF Access service token (its own) | +| Operator (human) | **No** | none | + +## Conformance (what a compliant template/service must show) + +- [ ] Cites `chittycanon://core/contracts/credential-access`. +- [ ] Contains no reference to 1Password / `op run` / `op read` as a secret source. +- [ ] Consumer surfaces contain no `CF_ACCESS_CLIENT_*`. +- [ ] No secret-value reads in a consumer surface (`SECRETS_STORE.get`, `secrets_resolve`, + `wrangler secret get`, `printenv|grep TOKEN`, `echo $*_SECRET`). +- [ ] Secrets in Secrets Store; names `CHITTYAUTH_ISSUED_*`; none in `[vars]`/KV. +- [ ] Fail-closed on broker-down with a canonical `POLICY_BLOCKED_*` code. + +## Enforcement + +CI gate `check-credential-contract.sh` (ChittyGuardian, non-bypassable) asserts the +conformance list above on every template and service. A template that restates the +rules instead of citing this URI, or that names 1Password, or that puts +`CF_ACCESS_CLIENT_*` in a consumer, fails the merge. diff --git a/plugins/chittyagent-dispatch/scripts/check-credential-contract.sh b/plugins/chittyagent-dispatch/scripts/check-credential-contract.sh new file mode 100644 index 0000000..f6c9d5c --- /dev/null +++ b/plugins/chittyagent-dispatch/scripts/check-credential-contract.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# ChittyGuardian check — Credential Access Contract conformance. +# canon: chittycanon://core/contracts/credential-access +# +# Usage: check-credential-contract.sh [PATH] (PATH defaults to ".") +# Exit 0 = pass, 1 = violations found. Intended as a non-bypassable CI gate. +set -uo pipefail + +ROOT="${1:-.}" +CONTRACT_URI="chittycanon://core/contracts/credential-access" +CODE_GLOBS=(--include='*.gs' --include='*.js' --include='*.ts' --include='*.py') +DOC_GLOBS=(--include='*.md' --include='*.json' --include='*.toml' --include='*.yaml' --include='*.yml') +fail=0 +note() { printf '%s\n' "$*" >&2; } + +# 1) Retired secret manager must not be named as a SOURCE. Allow explicit +# retirement/deprecation notes (so canon docs that say "1Password is RETIRED" pass). +onep=$(grep -rIniE '1password|op run|op read|op item get' "$ROOT" "${CODE_GLOBS[@]}" "${DOC_GLOBS[@]}" 2>/dev/null \ + | grep -viE 'retir|deprecat|legacy|stale|do not (use|follow)|no longer') +if [ -n "$onep" ]; then note "FAIL (1Password as source of truth):"; printf '%s\n' "$onep" >&2; fail=1; fi + +# 2) Consumer surfaces must not carry CF Access client secrets (broker-only identity). +cfa=$(grep -rInE 'CF_ACCESS_CLIENT_(ID|SECRET)' "$ROOT" "${CODE_GLOBS[@]}" 2>/dev/null) +if [ -n "$cfa" ]; then note "FAIL (consumer holds CF_ACCESS_CLIENT_* — broker identity):"; printf '%s\n' "$cfa" >&2; fail=1; fi + +# 3) No raw secret-value reads from a consumer (Apps Script surfaces). +# Skip comment lines (doc references to the canonical mechanism are not calls). +rawread=$(grep -rInE 'wrangler secret get|secrets_resolve|SECRETS_STORE\.get|printenv[^|]*\|[^|]*TOKEN|echo[[:space:]]+\$[A-Z_]*(TOKEN|SECRET|KEY)' "$ROOT" --include='*.gs' 2>/dev/null \ + | grep -vE ':[0-9]+:[[:space:]]*([*]|//|#)') +if [ -n "$rawread" ]; then note "FAIL (secret-value read in a consumer surface):"; printf '%s\n' "$rawread" >&2; fail=1; fi + +# 4) Every SECURITY.md must cite the contract URI. +while IFS= read -r f; do + [ -z "$f" ] && continue + if ! grep -qF "$CONTRACT_URI" "$f"; then note "FAIL ($f does not cite $CONTRACT_URI)"; fail=1; fi +done < <(find "$ROOT" -iname 'SECURITY.md' 2>/dev/null) + +if [ "$fail" -eq 0 ]; then note "PASS: credential-access contract conformance ($ROOT)"; fi +exit "$fail"