Skip to content

feat(broker): implement capabilities + policy + ledger primitives (5/7, signing deferred to #228)#240

Open
chitcommit wants to merge 2 commits into
mainfrom
feat/broker-primitives-209-partial
Open

feat(broker): implement capabilities + policy + ledger primitives (5/7, signing deferred to #228)#240
chitcommit wants to merge 2 commits into
mainfrom
feat/broker-primitives-209-partial

Conversation

@chitcommit

Copy link
Copy Markdown
Contributor

Summary

Implements 5 of 7 broker primitives specified in CHARTER.md "Git Broker Surface (REST, sensitive) — SPEC" (#235). Option A from prior audit: ships unblocked work now; signing endpoints deferred to #228.

Why Option A

Signing requires 1Password key resolution + canonical commit/tag payload signing, both gated on #228. Rather than block all of #209, ship non-signing primitives now so chittyagent-git can integrate against a real broker for capability mint/introspect/policy/audit. Signing lands in follow-up when #228 closes.

Endpoints (5/7)

Method Path Purpose
POST /api/v1/capabilities/mint Mint short-TTL capability (≤300s) bound to caller × tenant × operation × repo × ref
POST /api/v1/capabilities/introspect Validate token + return scope claims
POST /api/v1/capabilities/confirm Single-use confirmation token (≤120s) for force-push gating
POST /api/v1/policy/resolve Resolve tenant policy: allowed_repos / allowed_remotes / force_push_allowed / protected_branches / default_author
POST /api/v1/ledger/emit Domain-tagged git event with canonical sha256; persists to broker_capability_audit (real D1 row — TODO forward to chittyledger#11)

Legacy /api/git/confirm + /api/git/confirm/* return 410 Gone with replacement pointer. src/api/routes/git-confirm.js deleted.

Storage (corrections from original brief, accepted from audit)

  • KV (TOKEN_KV) for tokens, prefixed broker:cap: / broker:confirm: — consistent with existing pattern
  • D1 (DB) for tenant policy + durable audit
  • Migration 019 extends 018 ADD-ONLY (no reshape):
    • git_tenant_policy (scalars per tenant)
    • git_protected_branches (tuple list)
    • broker_capability_audit (durable audit trail)

ajv schemas validated: 10/10

src/schemas/v1/{capabilities.{mint,introspect,confirm},policy.resolve,ledger.emit}.{input,output}.json. ChittyID pattern enforces canonical VV-G-LLL-SSSS-T-YYMM-C-XX per chittycanon://gov/governance with all five {P,L,T,E,A} accepted.

Tests — REAL D1 + KV

tests/api/v1/broker-primitives.test.js boots wrangler unstable_dev with local D1 + KV. Migrations applied via wrangler d1 execute DB --env=dev --local --file=.... No vi.mock on DB/KV/service modules per global rule.

 Test Files  1 passed (1)
      Tests  13 passed (13)
   Duration  ~17s

Coverage: policy.resolve seed read-back; schema enforcement (positive+negative ChittyID, push requires ref+remote, force requires confirmation_token); repo allowlist denial; protected-branch hard-deny; force-push denied when force_push_allowed=false even with valid confirmation token; mint→introspect roundtrip; ledger event_type↔operation binding; URL userinfo redaction; legacy 410.

Test plan

🤖 Generated with Claude Code

…7, signing deferred to #228)

Implements the broker-primitive REST surface specified in CHARTER.md
"Git Broker Surface (REST, sensitive) — SPEC" (#235).

Scope (Option A from audit — ships 5 of 7 primitives now):
  POST /api/v1/capabilities/mint        — part of #209
  POST /api/v1/capabilities/introspect  — part of #209
  POST /api/v1/capabilities/confirm     — closes #210 (supersedes /api/git/confirm)
  POST /api/v1/policy/resolve           — closes #211
  POST /api/v1/ledger/emit              — part of #209 (TODO: forward to chittyledger#11)

Signing primitives (/api/v1/signing/sign-commit, /sign-tag) are deferred to
#228#209 stays open until those land.

Storage:
  - TOKEN_KV  → live opaque capability + confirmation tokens (short-TTL, KV
                consistency with existing git-confirm pattern)
  - DB (D1)   → tenant policy (018 + new 019 extensions) + broker audit trail

Migration 019 extends 018 in place (ADD ONLY, no reshape):
  - git_tenant_policy            — scalar per-tenant fields (force_push_allowed,
                                   default_author) that don't fit 018's tuple tables
  - git_protected_branches       — protected_branches list per CHARTER §4
  - broker_capability_audit      — durable audit trail (also serves as the
                                   real-behavior ledger backing /ledger/emit
                                   until chittyledger#11 lands)

Schemas (ajv): 10 schemas (input + output for all 5 primitives) under
src/schemas/v1/. ChittyID pattern enforces canonical VV-G-LLL-SSSS-T-YYMM-C-XX
per chittycanon://gov/governance.

Deprecation: /api/git/confirm + /api/git/confirm/* return 410 Gone with
replacement pointer (registered ABOVE auth so legacy callers get a clear
signal without an API key). src/api/routes/git-confirm.js deleted.

Tests: 13 integration tests via wrangler unstable_dev with REAL local D1 +
KV — no mocks on DB, KV, or service modules. Migrations applied via
`wrangler d1 execute DB --env=dev --local --file=...` in beforeAll; API key
seeded into local API_KEYS KV for mcpAuthMiddleware.

  Test Files  1 passed (1)
       Tests  13 passed (13)
    Duration  ~17s

Validated:
  - All 10 ajv schemas compile + validate positive and negative cases
  - policy.resolve returns seeded chittyos-default tenant policy
  - capabilities.mint + introspect roundtrip with real KV
  - repo allowlist enforcement (POLICY_BLOCKED_REPO_NOT_ALLOWED)
  - schema-level enforcement of push requirements (ref + remote + confirmation)
  - protected-branch hard-deny (POLICY_BLOCKED_FORCE_TO_PROTECTED) on main
  - force_push_allowed=false denies even with valid confirmation token
  - ledger.emit operation/event_type binding (POLICY_BLOCKED_CAPABILITY_INVALID)
  - URL userinfo redaction in ledger payloads
  - legacy /api/git/confirm returns 410 + replacement pointer

Closes #210
Closes #211
Partial of #209 (signing primitives blocked on #228)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 4, 2026 13:34
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
chittyconnect 2b4f4dd Jun 04 2026, 01:55 PM

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@chitcommit, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 1 minute and 49 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2b55394-c03f-4068-8fa6-223293ef6045

📥 Commits

Reviewing files that changed from the base of the PR and between ca9642c and 2b4f4dd.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • migrations/019_policy_resolve_extensions.sql
  • package.json
  • src/api/routes/broker-primitives.js
  • src/api/routes/git-confirm.js
  • src/index.js
  • src/schemas/v1/capabilities.confirm.input.json
  • src/schemas/v1/capabilities.confirm.output.json
  • src/schemas/v1/capabilities.introspect.input.json
  • src/schemas/v1/capabilities.introspect.output.json
  • src/schemas/v1/capabilities.mint.input.json
  • src/schemas/v1/capabilities.mint.output.json
  • src/schemas/v1/index.js
  • src/schemas/v1/ledger.emit.input.json
  • src/schemas/v1/ledger.emit.output.json
  • src/schemas/v1/policy.resolve.input.json
  • src/schemas/v1/policy.resolve.output.json
  • tests/api/v1/broker-primitives.test.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/broker-primitives-209-partial

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 332f9cf33b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"Tenant policy disallows force-push",
);
}
if (ref && policy.protected_branches.includes(ref)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hard-deny main/master even when tenant rows are absent

For tenants that have force_push_allowed=1 but do not have explicit git_protected_branches rows for refs/heads/main and refs/heads/master (for example any newly added tenant, since the migration only seeds chittyos-default), this check lets a confirmed force capability be minted for the default branch. The removed legacy route hard-denied main/master independently of tenant data, and the new migration comment says missing branch rows should still honor those hard-deny defaults, so the mint path needs a built-in main/master guard as well as the DB list.

Useful? React with 👍 / 👎.

);
}

const repoOk = policy.repos.some((r) => prefixMatch(r.path_prefix, repo_path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce repo access before minting write capabilities

When a tenant has a matching git_repo_allowlist row with access='read', this predicate still accepts it for commit, tag, and push operations because it only checks the path prefix. That lets a read-only repo allowlist grant write-capability tokens; the operation should require write/readwrite for write operations rather than treating every matching row as sufficient.

Useful? React with 👍 / 👎.

Comment thread src/api/routes/broker-primitives.js Outdated
}

if (operation === "push") {
const remoteOk = policy.remotes.some((r) => globMatch(r.pattern, remote));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor remote allowlist match_type values

This always applies glob matching and ignores the match_type column loaded from git_remote_allowlist. Rows configured with match_type='prefix' are valid per migration 018, but a pattern like https://github.com/CHITTYOS/ will not match https://github.com/CHITTYOS/repo.git here unless it also contains a wildcard, so legitimate tenant push policies using prefix matching are denied.

Useful? React with 👍 / 👎.

Comment thread src/api/routes/broker-primitives.js Outdated
}

if (operation === "push") {
const remoteOk = policy.remotes.some((r) => globMatch(r.pattern, remote));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect per-remote force gates before minting

This reduces the matching remote row to a boolean and then the force path only checks the tenant-wide force_push_allowed flag, so a tenant with global force enabled can mint a force capability for any allowed remote even when that row has allow_force=0. Migration 018 defines allow_force as the per-remote gate for force pushes, and the seeds default it to 0, so the mint logic should require the matched remote to allow force before accepting the confirmation token.

Useful? React with 👍 / 👎.

Comment thread src/index.js
* ledger emit. Closes chittyos/chittyconnect#210, #211. Partial of #209
* (signing endpoints deferred to chittyos/chittyconnect#228).
*/
app.route("/api/v1", brokerPrimitivesRoutes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid applying broker auth to later /api/v1 routes

Mounting brokerPrimitivesRoutes here also mounts its broker.use('*', mcpAuthMiddleware) wildcard under /api/v1, so requests to the existing secrets endpoints registered below this point (for example /api/v1/secrets/status, /rotate, and /upsert) now pass through MCP auth after their existing /api/v1/secrets/* authenticate middleware. In the Bearer/OAuth or Cloudflare-Access-only upsert contexts handled by authenticate, these requests have no X-ChittyOS-API-Key and are rejected before reaching the secrets handler, breaking the documented secrets API.

Useful? React with 👍 / 👎.

);
}

const repoOk = policy.repos.some((r) => prefixMatch(r.path_prefix, repo_path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Canonicalize repo paths before prefix checks

Because this compares the raw request string, a repo path such as /home/ubuntu/projects/github.com/CHITTYOS/../outside/repo still starts with the seeded ChittyOS allowlist prefix even though it resolves outside that tree. Migration 018 says repo prefixes are canonicalized at check time; without normalizing and rejecting traversal before minting, the resource server can receive a valid capability for a path outside the tenant's allowed repo scope.

Useful? React with 👍 / 👎.

Comment thread src/api/routes/broker-primitives.js Outdated
Comment on lines +174 to +175
} catch (e) {
console.error("[broker-audit] insert failed:", e.message);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail closed when audit inserts fail

When the D1 insert throws, this helper only logs the error and all callers continue returning success; for example /ledger/emit can return a ledger_event_id even though no durable broker_capability_audit row was written. On this sensitive broker surface the audit row is the durable ledger placeholder, so successful mint/confirm/introspect/ledger responses should fail closed or return an unavailable error when audit persistence fails.

Useful? React with 👍 / 👎.

import { validate } from "../../schemas/v1/index.js";

const broker = new Hono();
broker.use("*", mcpAuthMiddleware);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept bearer auth on broker primitives

The broker is wired to mcpAuthMiddleware, which only reads X-ChittyOS-API-Key; it ignores Authorization: Bearer even though the CHARTER authentication section allows bearer tokens and the existing API authenticate middleware supports that path. A resource server or client configured with bearer auth will get a 401 before reaching any of the new primitives, so this route should either use the shared auth middleware or extend MCP auth to honor bearer tokens.

Useful? React with 👍 / 👎.


const { capability_token, event_type, payload } = body;

const capRaw = await c.env.TOKEN_KV.get(CAP_PREFIX + capability_token);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check stored capability expiry in ledger emit

/capabilities/introspect rejects records whose expires_at is in the past, but /ledger/emit accepts any capability record still present in KV. If a record remains after its timestamp expires (for example in local persistent KV, manual repair, or delayed expiration), an expired capability can still emit a ledger event even though the broker's own error table classifies expired capabilities as invalid.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the new Git broker REST surface under /api/v1/* (capability mint/introspect/confirm, policy resolve, ledger emit), adds AJV schemas + D1 migrations for policy/audit storage, adds integration tests against real local D1+KV, and deprecates legacy /api/git/confirm endpoints with 410 Gone responses.

Changes:

  • Add broker-primitives route module implementing 5/7 broker primitives (signing deferred).
  • Add v1 JSON Schemas (input/output) and AJV compile/validate helper.
  • Add D1 migration 019 for policy scalar fields, protected branches, and durable audit trail; add integration test suite; deprecate legacy endpoints.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/api/routes/broker-primitives.js New /api/v1/* broker primitives implementation (capabilities, policy, ledger emit) backed by KV + D1.
src/index.js Mount broker primitives under /api/v1 and replace legacy git-confirm routes with unauthenticated 410 Gone.
src/schemas/v1/index.js AJV compiler + exported validators for v1 broker schemas.
src/schemas/v1/*.json JSON Schemas for broker primitive request/response envelopes.
migrations/019_policy_resolve_extensions.sql Adds git_tenant_policy, git_protected_branches, and broker_capability_audit tables + seeds.
tests/api/v1/broker-primitives.test.js Integration tests using wrangler unstable_dev with real local D1 + KV.
package.json / package-lock.json Adds ajv + ajv-formats dependencies / lock updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +239 to +259
const repoOk = policy.repos.some((r) => prefixMatch(r.path_prefix, repo_path));
if (!repoOk) {
await auditEvent(c.env, {
event_type: "mint",
caller_chittyid,
tenant_id,
operation,
repo_path,
remote,
ref,
force_class,
outcome: "denied",
reason_code: "POLICY_BLOCKED_REPO_NOT_ALLOWED",
});
return jerror(
c,
403,
"POLICY_BLOCKED_REPO_NOT_ALLOWED",
"repo_path is not in tenant repo allowlist",
);
}
Comment on lines +261 to +283
if (operation === "push") {
const remoteOk = policy.remotes.some((r) => globMatch(r.pattern, remote));
if (!remoteOk) {
await auditEvent(c.env, {
event_type: "mint",
caller_chittyid,
tenant_id,
operation,
repo_path,
remote,
ref,
force_class,
outcome: "denied",
reason_code: "POLICY_BLOCKED_REMOTE_NOT_ALLOWED",
});
return jerror(
c,
403,
"POLICY_BLOCKED_REMOTE_NOT_ALLOWED",
"Remote URL not in tenant allowlist",
);
}
}
Comment on lines +326 to +347
const ckey = CONFIRM_PREFIX + confirmation_token;
const craw = await c.env.TOKEN_KV.get(ckey);
if (!craw) {
return jerror(
c,
403,
"POLICY_BLOCKED_CONFIRMATION_INVALID",
"Confirmation token missing or expired",
);
}
let crec;
try {
crec = JSON.parse(craw);
} catch {
await c.env.TOKEN_KV.delete(ckey);
return jerror(
c,
403,
"POLICY_BLOCKED_CONFIRMATION_INVALID",
"Confirmation token unreadable",
);
}
);
}

if (policy.protected_branches.includes(ref)) {
Comment on lines +285 to +305
if (force_class !== "none") {
if (!policy.force_push_allowed) {
await auditEvent(c.env, {
event_type: "mint",
caller_chittyid,
tenant_id,
operation,
repo_path,
remote,
ref,
force_class,
outcome: "denied",
reason_code: "POLICY_BLOCKED_FORCE_TO_PROTECTED",
});
return jerror(
c,
403,
"POLICY_BLOCKED_FORCE_TO_PROTECTED",
"Tenant policy disallows force-push",
);
}
ref,
confirmation_token,
} = body;
const force_class = body.force_class || "none";
Comment on lines +40 to +46
{
"if": {
"properties": { "force_class": { "enum": ["force", "force_with_lease"] } },
"required": ["force_class"]
},
"then": { "required": ["confirmation_token"] }
}
Comment on lines +193 to +199
function redactPayload(obj) {
if (obj === null || typeof obj !== "object") return redactString(obj);
if (Array.isArray(obj)) return obj.map(redactPayload);
const out = {};
for (const k of Object.keys(obj)) out[k] = redactPayload(obj[k]);
return out;
}
ledger_event_id: ledgerEventId,
domain: "git",
event_type,
scope: capRec.scope,
Comment thread src/api/routes/broker-primitives.js Outdated
Comment on lines +679 to +680
remote: capRec.scope.remote,
ref: capRec.scope.ref,

@chitcommit chitcommit left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (request-changes equivalent — bot cannot self-request-changes)

Multi-reviewer pass (code-reviewer / silent-failure-hunter / pr-test-analyzer / comment-analyzer). 6 critical, 3 important, 3 doc-rot findings. Inline annotations below.

Critical

  1. Hard-deny of main/master regressed to be tenant-config dependent — tenants without explicit git_protected_branches rows can force-push to main; bare names (no refs/heads/ prefix) never blocked.
  2. Expired-token check fails open on missing/corrupt expires_at (NaN < Date.now() === false).
  3. auditEvent swallows D1 failures with console.error only — ok mint/confirm/ledger_emit can issue tokens while the durable audit row silently fails.
  4. /policy/resolve schema requires caller_chittyid+operation but handler reads only tenant_id — any authenticated caller can dump any tenant's policy bundle. No audit row emitted.
  5. /capabilities/confirm only checks protected_branches; ignores policy.force_push_allowed — a tenant with force globally disabled can still mint confirmation tokens.
  6. Test gap: no redeem-with-mismatched-scope test (#235 fix not covered), no cross-tenant replay test (#210 reopen reason), no expired-token introspect test. Several existing tests are ajv-only where behavior matters.

Important

  1. git_remote_allowlist.match_type selected but ignored — every entry silently treated as glob.
  2. Migration 019 audit enum allows expired/invalid but no code path writes them.
  3. KV put on mint/confirm not wrapped — silent persistence failure possible.

Doc rot

  1. TODO references chittyledger#11 (a merged Dependabot PR, not a tracking issue).
  2. src/index.js:1551 cites #228 for signing-deferral; #228 is the credentials issue. Signing-deferral is #242.
  3. ChittyID schema description says YYMM, regex enforces [0-9]{4}, canon (chittycanon://gov/governance) says YM. Description and regex agree; canon disagrees with both. Recommend follow-up issue rather than silent edit.

Fixes incoming on the same branch.

— attribution: code-reviewer (1,3,5,7,9), silent-failure-hunter (2,3,8,9), pr-test-analyzer (6), comment-analyzer (10,11,12), shared (4).

"Tenant policy disallows force-push",
);
}
if (ref && policy.protected_branches.includes(ref)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL #1 (code-reviewer + silent-failure-hunter)policy.protected_branches.includes(ref) is now tenant-config dependent. The chittyos-default seed includes refs/heads/main and refs/heads/master, but any other tenant without explicit rows in git_protected_branches can force-push to main. Additionally, bare main/master (no refs/heads/ prefix) are never blocked even for the seeded tenant. CHARTER says "Force-push to main/master is hard-denied" — that must be a universal constant, not data-driven. Fix: union per-tenant rows with universal set ["main","master","refs/heads/main","refs/heads/master"] in loadTenantPolicy. Apply to both mint and confirm code paths.

Comment thread src/api/routes/broker-primitives.js Outdated
return c.json({ active: false });
}

if (new Date(rec.expires_at).getTime() < Date.now()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL #2 (silent-failure-hunter) — Fails open on bad/missing expires_at. new Date(undefined).getTime() returns NaN; NaN < Date.now() is false, so a record with a corrupt or missing expires_at is treated as active and the caller's scope is returned. Validate that parsed expires_at is a finite number; treat NaN/missing as expired (active:false).

Comment thread src/api/routes/broker-primitives.js Outdated
)
.run();
} catch (e) {
console.error("[broker-audit] insert failed:", e.message);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL #3 (silent-failure-hunter)auditEvent catches D1 errors with console.error only. On outcome:"ok" for mint/confirm/ledger_emit, callers receive issued tokens or ledger ids even when the durable audit row silently fails — breaking the audit-before-issue guarantee. Refactor: throw on D1 failure for success paths; route handlers should return 503 AUDIT_WRITE_FAILED. Keep best-effort for denied/expired/invalid outcomes with tagged console.error.

protected_branches: policy.protected_branches,
...(policy.default_author ? { default_author: policy.default_author } : {}),
};
return c.json(out);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL #4 (shared) — Handler reads only tenant_id; caller_chittyid and operation from the schema are unused. Any authenticated caller can dump any tenant's full policy bundle (allowed_repos, allowed_remotes, default_author, protected_branches). No audit row is emitted for this endpoint either. Fix: enforce caller-tenant membership (note: no membership table exists today — fail closed with a clear reason code and follow-up issue), and emit event_type:"policy_resolve" audit (extend migration 019 CHECK constraint).

"POLICY_BLOCKED_FORCE_TO_PROTECTED",
"Force-push to protected branch is hard-denied",
);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL #5 (code-reviewer)/capabilities/confirm only checks protected_branches. It never reads policy.force_push_allowed, so a tenant where force-push is globally disabled at the scalar policy level can still successfully mint a confirmation token. Mint catches the second gate later, but confirm should fail-closed earlier with POLICY_BLOCKED_FORCE_DISABLED.

remote TEXT,
ref TEXT,
force_class TEXT,
outcome TEXT NOT NULL CHECK(outcome IN ('ok','denied','expired','invalid')),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMPORTANT #8 (silent-failure-hunter)outcome CHECK allows expired and invalid but no code path writes them. Introspect on an expired record returns 200/active:false with no audit row. Confirm/ledger_emit JSON parse failures return 403 with no audit row. Forensic gap. Emit outcome:"expired" on the introspect-expired branch and outcome:"invalid" on parse-failure branches.

Comment thread src/api/routes/broker-primitives.js Outdated

await c.env.TOKEN_KV.put(CAP_PREFIX + token, JSON.stringify(record), {
expirationTtl: CAPABILITY_TTL_SECONDS,
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMPORTANT #9 (silent-failure-hunter)TOKEN_KV.put on mint and confirm is not wrapped in try/catch. If KV is degraded, the token returned to the caller is never persisted — caller treats it as live, introspect/confirm/emit all return 403. Wrap with try/catch; on failure return 503 POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE and audit denied with reason TOKEN_STORE_UNAVAILABLE.

Comment thread src/api/routes/broker-primitives.js Outdated
// (durable D1 record). When the chittyledger domain handshake lands, the
// audit row will be forwarded to ChittyLedger and the ledger_event_id will
// be replaced with the upstream id.
// TODO(chittyledger#11): forward entry to ChittyLedger domain projection

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOC ROT #10 (comment-analyzer) — TODO references chittyledger#11, which is a merged Dependabot PR in that repo, not a tracking issue. Replace with the real tracking issue or remove the bracketed reference.

Comment thread src/index.js Outdated
/**
* Broker primitives v1 — capability mint/introspect/confirm, policy resolve,
* ledger emit. Closes chittyos/chittyconnect#210, #211. Partial of #209
* (signing endpoints deferred to chittyos/chittyconnect#228).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOC ROT #11 (comment-analyzer) — Cites chittyos/chittyconnect#228 as the signing-deferral. #228 is the credentials/secrets issue; the signing-deferral issue is #242. Update reference.

"caller_chittyid": {
"type": "string",
"pattern": "^[A-Z0-9]{2}-[A-Z0-9]-[A-Z0-9]{3}-[A-Z0-9]{4}-[PLTEA]-[0-9]{4}-[0-5]-[0-9]{2}$",
"description": "Canonical ChittyID VV-G-LLL-SSSS-T-YYMM-C-XX per chittycanon://gov/governance."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOC ROT #12 (comment-analyzer) — Description says VV-G-LLL-SSSS-T-YYMM-C-XX and the regex enforces [0-9]{4}-[0-5]-[0-9]{2} (consistent with the description). However the canonical ontology at chittycanon://gov/governance says VV-G-LLL-SSSS-T-YM-C-X (compact YM, single-digit C, single-digit X). Schema/description are internally consistent; canon disagrees with both. Recommend a follow-up reconciliation issue rather than silently changing either side — every existing fixture (CC-A-CCC-0001-P-2606-3-42) matches the schema, not canon.

…portant, 3 doc-rot

Addresses inline review at #240. All real fixes — no
mocks, no placeholders, no fake data. 19/19 broker tests pass (3 new),
461/461 suite passes.

Critical:
- #1 Hard-deny main/master is now a UNIVERSAL_PROTECTED_BRANCHES constant
  unioned into every tenant policy. Both bare (main/master) and prefixed
  (refs/heads/*) forms covered. Applied to mint AND confirm.
- #2 Introspect now validates expires_at parses to a finite number. NaN
  (the previous fail-open) is treated as expired+invalid with audit.
- #3 auditEvent split into best-effort (denied/expired/invalid) and
  critical (ok mint/confirm/ledger_emit). Critical throws → 503
  AUDIT_WRITE_FAILED → KV rollback. Preserves audit-before-issue invariant.
- #4 /policy/resolve now requires caller-tenant membership. No membership
  table exists in 016 yet (tenant_projects maps tenant→Neon only), so per
  the task constraint we fail-closed to canonical chittyos-default with a
  clear POLICY_BLOCKED_TENANT_MEMBERSHIP_UNVERIFIED reason. Tracking
  follow-up needed before widening. Emits policy_resolve audit rows.
- #5 /capabilities/confirm now rejects when policy.force_push_allowed is
  false with POLICY_BLOCKED_FORCE_DISABLED, not just at mint time.
- #6 Three new real-D1+KV tests added: expired-token introspect, four
  scope-mismatch redeem cases (#235 fix coverage), cross-tenant replay
  (#210 reopen reason).

Important:
- #7 git_remote_allowlist.match_type now dispatched (exact/prefix/glob);
  unknown match_type fails closed instead of falling through to glob.
- #8 migration 019 CHECK constraint widened to allow expired/invalid/
  policy_resolve outcomes; route handlers now emit them.
- #9 TOKEN_KV.put on mint+confirm wrapped; KV failure returns 503
  POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE with TOKEN_STORE_UNAVAILABLE
  audit row.

Doc rot:
- #10 Removed incorrect chittyledger#11 reference (that was a merged
  Dependabot PR, not a tracking issue).
- #11 src/index.js + broker-primitives.js header now reference #242 for
  signing-deferral; #228 is the credentials/secrets issue.
- #12 ChittyID schema description left as-is (YYMM, matches regex);
  reported as canon-vs-schema mismatch needing reconciliation rather than
  silent edit — every existing fixture matches schema, not canon.

Tracks: #209 #210 #211 #235

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b4f4ddf0d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +461 to +467
const scopeMatches =
crec.caller_chittyid === caller_chittyid &&
crec.tenant_id === tenant_id &&
crec.repo_path === repo_path &&
crec.remote === remote &&
crec.ref === ref &&
crec.force_class === force_class;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject stale confirmation records before minting

When TOKEN_KV still returns a confirmation record after its embedded expires_at has passed (for example in local persistent KV, delayed/manual KV repair, or a stale restored record), this path only checks the bound scope and then mints a force capability. Because confirmation tokens are the freshness check for force pushes and are supposed to be valid for at most 120 seconds, redemption should compare crec.expires_at to Date.now() and fail closed before accepting the token.

Useful? React with 👍 / 👎.

Comment on lines +327 to +329
let policy;
try {
policy = await loadTenantPolicy(c.env, tenant_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify caller tenant membership before minting

For any authenticated API key, this loads policy for the tenant_id supplied in the request body and proceeds to mint if that tenant's repo/remote rows match; it never checks that the authenticated caller or claimed caller_chittyid belongs to that tenant. The new policy.resolve route explicitly fails closed because tenant membership cannot be verified, but the mint path skips the same gate, so an API-key holder who knows another configured tenant's repo prefix can obtain capability tokens under that tenant.

Useful? React with 👍 / 👎.

Comment thread src/schemas/v1/index.js
addFormats(ajv);

export const validators = {
"capabilities.mint.input": ajv.compile(capabilitiesMintInput),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid runtime Ajv compilation in the Worker

These module-scope ajv.compile(...) calls generate validator functions with dynamic code evaluation during Worker startup. Cloudflare Workers reject eval/new Function unless startup eval is explicitly enabled, and the inspected wrangler.jsonc compatibility flags only include nodejs_compat and global_fetch_strictly_public, so importing this route can make the deployed Worker fail to start; precompile the schemas or enable the required startup-eval path intentionally.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants