Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions canonical/contracts/credential-access.md
Original file line number Diff line number Diff line change
@@ -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, "<service>")` or
`await env.SECRETS_STORE.get("<NAME>")` (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_<SERVICE>_<TOKEN|API_KEY>` (legacy `CHITTY_<SERVICE>_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.
39 changes: 39 additions & 0 deletions plugins/chittyagent-dispatch/scripts/check-credential-contract.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading