From bdfae64cd6941907301c29cfcbd55a382d9297b3 Mon Sep 17 00:00:00 2001 From: Dukeabadoon Date: Wed, 12 Aug 2026 13:04:10 +0800 Subject: [PATCH 1/7] docs: add production plan and gate for v0.2.0 Document phased path from preview to production-ready broker, controller, ledger, and release checks. --- README.md | 2 + docs/PRODUCTION_GATE.md | 55 +++++++++++++++++++++ docs/PRODUCTION_PLAN/overview.md | 42 ++++++++++++++++ docs/PRODUCTION_PLAN/phase-0-docs.md | 18 +++++++ docs/PRODUCTION_PLAN/phase-1-broker.md | 40 +++++++++++++++ docs/PRODUCTION_PLAN/phase-2-permissions.md | 24 +++++++++ docs/PRODUCTION_PLAN/phase-3-controller.md | 27 ++++++++++ docs/PRODUCTION_PLAN/phase-4-ledger.md | 21 ++++++++ docs/PRODUCTION_PLAN/phase-5-storage.md | 25 ++++++++++ docs/PRODUCTION_PLAN/phase-6-release.md | 20 ++++++++ 10 files changed, 274 insertions(+) create mode 100644 docs/PRODUCTION_GATE.md create mode 100644 docs/PRODUCTION_PLAN/overview.md create mode 100644 docs/PRODUCTION_PLAN/phase-0-docs.md create mode 100644 docs/PRODUCTION_PLAN/phase-1-broker.md create mode 100644 docs/PRODUCTION_PLAN/phase-2-permissions.md create mode 100644 docs/PRODUCTION_PLAN/phase-3-controller.md create mode 100644 docs/PRODUCTION_PLAN/phase-4-ledger.md create mode 100644 docs/PRODUCTION_PLAN/phase-5-storage.md create mode 100644 docs/PRODUCTION_PLAN/phase-6-release.md diff --git a/README.md b/README.md index 6911794..fabda0a 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,7 @@ See [Contributing](CONTRIBUTING.md) for the artifact-first workflow. - [API reference](docs/API.md) - [Adapter setup](docs/ADAPTERS.md) - [Current status](docs/STATUS.md) +- [Production gate](docs/PRODUCTION_GATE.md) - [Security](SECURITY.md) - [Privacy](PRIVACY.md) @@ -370,3 +371,4 @@ reproducible, non-sensitive defects. ## License Telic is released under the [MIT License](LICENSE). + diff --git a/docs/PRODUCTION_GATE.md b/docs/PRODUCTION_GATE.md new file mode 100644 index 0000000..eef81a8 --- /dev/null +++ b/docs/PRODUCTION_GATE.md @@ -0,0 +1,55 @@ +# Telic production gate + +Checklist for declaring **v0.2.0** production-ready preview. Every row must pass before tagging. + +**Last updated:** 2026-08-12 + +## Trust boundary + +| # | Criterion | Verify | +|---|-----------|--------| +| T1 | Broker hooks fail-closed when `TELIC_BROKER_STRICT=1` (default in adapter hooks) | `npm test -- packages/cli/src/broker-gate.test.ts test/cagt-broker-hook-e2e.test.ts` | +| T2 | Hook denies when `telic` CLI is missing (strict) | E2E or hook unit test | +| T3 | README and STATUS state Telic does not cage the host without hooks | Manual read | + +## Maintainability + +| # | Criterion | Verify | +|---|-----------|--------| +| M1 | `intersectStructuredPermissions` has one implementation | `rg intersectStructuredPermissions packages/core/src` shows one definition | +| M2 | `controller.ts` facade under 1,500 lines; validators extracted | `wc -l packages/core/src/controller.ts` | +| M3 | Extracted validators have unit tests | `ls packages/core/src/controller/*.test.ts` or `validators/` | + +## Quality + +| # | Criterion | Verify | +|---|-----------|--------| +| Q1 | Full test suite passes | `npm test` | +| Q2 | Adapter handshakes pass | `npm run adapters:validate` | +| Q3 | Site builds (if web touched) | `npm run site:check` | +| Q4 | Coverage run stable (ledger concurrency) | `npm test -- --coverage` three consecutive green runs | + +## Review + +| # | Criterion | Verify | +|---|-----------|--------| +| R1 | `/review` or equivalent on full diff vs `main` | PR review notes | +| R2 | Bugbot triaged (fix or dismiss with reason) | PR comments | +| R3 | Security review on broker + permissions (Phase 1) | security-review artifact | + +## Release + +| # | Criterion | Verify | +|---|-----------|--------| +| S1 | `CHANGELOG.md` updated for v0.2.0 | File diff | +| S2 | Workspace + `telic-mcp` version `0.2.0` | `package.json` | +| S3 | Git tag `v0.2.0` on `main` | `git tag -l v0.2.0` | +| S4 | `main` CI green after merge | `gh pr checks` or Actions | + +## Explicitly out of scope for v0.2.0 + +- CAGT / protocol rewrite +- Forensic-only FSM expansion beyond current ADRs +- Adapter marketplace lifecycle certification +- Parallel WorkPlan execution +- Hosted service or telemetry diff --git a/docs/PRODUCTION_PLAN/overview.md b/docs/PRODUCTION_PLAN/overview.md new file mode 100644 index 0000000..52d1152 --- /dev/null +++ b/docs/PRODUCTION_PLAN/overview.md @@ -0,0 +1,42 @@ +# Telic production readiness plan + +**Target:** v0.2.0 production-ready preview +**Repo:** Developer Tools (Telic) +**Base branch:** `main` + +## Context + +System review graded protocol and tests highly (S), but security posture (B) and controller complexity (C+) need work before a credible production tag. This plan sequences fixes without rewriting CAGT or the protocol. + +## Scope + +**In:** fail-closed broker, permission dedup, controller split, ledger hardening, optional storage GC, release gate. + +**Out:** CAGT redesign, forensic FSM expansion, new adapters, parallel workers. + +## Phases + +| Phase | Doc | Branch | Playbook | +|-------|-----|--------|----------| +| 0 | [phase-0-docs.md](./phase-0-docs.md) | `feat/production-phase-0` | multi-phase-plan | +| 1 | [phase-1-broker.md](./phase-1-broker.md) | `feat/production-phase-1-broker` | feature | +| 2 | [phase-2-permissions.md](./phase-2-permissions.md) | `feat/production-phase-2-permissions` | refactoring | +| 3 | [phase-3-controller.md](./phase-3-controller.md) | `feat/production-phase-3a` … `3c` | refactoring | +| 4 | [phase-4-ledger.md](./phase-4-ledger.md) | `feat/production-phase-4-ledger` | bug-fix | +| 5 | [phase-5-storage.md](./phase-5-storage.md) | `feat/production-phase-5-storage` | feature | +| 6 | [phase-6-release.md](./phase-6-release.md) | `release/v0.2.0` | shipping | + +## Per-phase gate + +```text +/freeze +→ implement +→ npm test && npm run adapters:validate +→ /review + Bugbot (+ security-review on Phase 1) +→ /ship PR +→ merge when CI green +``` + +## Done predicate + +All rows in [PRODUCTION_GATE.md](../PRODUCTION_GATE.md) pass. Tag `v0.2.0` on `main`. diff --git a/docs/PRODUCTION_PLAN/phase-0-docs.md b/docs/PRODUCTION_PLAN/phase-0-docs.md new file mode 100644 index 0000000..3cbc241 --- /dev/null +++ b/docs/PRODUCTION_PLAN/phase-0-docs.md @@ -0,0 +1,18 @@ +# Phase 0 — Production docs + +## Deliverables + +- `docs/PRODUCTION_GATE.md` +- `docs/PRODUCTION_PLAN/overview.md` + phase files +- README link to production gate + +## Exit criteria + +- [ ] All files exist +- [ ] `npm test` still passes (docs only) + +## Verify + +```bash +test -f docs/PRODUCTION_GATE.md && test -f docs/PRODUCTION_PLAN/overview.md +``` diff --git a/docs/PRODUCTION_PLAN/phase-1-broker.md b/docs/PRODUCTION_PLAN/phase-1-broker.md new file mode 100644 index 0000000..7b09338 --- /dev/null +++ b/docs/PRODUCTION_PLAN/phase-1-broker.md @@ -0,0 +1,40 @@ +# Phase 1 — Fail-closed broker + +## Goal + +When adapter hooks run with strict mode, deny risky tools if Telic cannot evaluate permissions. + +## Files + +- `packages/cli/src/broker-gate.ts` +- `packages/cli/src/broker-gate.test.ts` +- `adapters/cursor/project/.cursor/hooks/broker-gate.mjs` +- `adapters/cline/project/.cline/hooks/broker-gate.mjs` +- `adapters/roo-code/project/.roo/hooks/broker-gate.mjs` +- `plugins/telic/hooks/broker-gate.mjs` +- `test/cagt-broker-hook-e2e.test.ts` +- `docs/STATUS.md`, `README.md`, `docs/ADAPTERS.md` + +## Data shape + +`HookPermissionResponse`: `{ permission: "allow" | "deny"; user_message?; agent_message? }` + +Strict mode via `TELIC_BROKER_STRICT=1` (set in hook scripts). Opt-out: `TELIC_BROKER_PERMISSIVE=1`. + +## Behavior changes + +1. Hook fallback when CLI missing → `deny` (strict). +2. `evaluateBrokerGate`: when strict and mapped capability with no valid active session → `deny`. +3. When strict and run version mismatch / not running → `deny` (not allow). + +## Exit criteria + +- PRODUCTION_GATE T1–T3 +- All tests pass + +## Verify + +```bash +npm test -- packages/cli/src/broker-gate.test.ts test/cagt-broker-hook-e2e.test.ts +npm run adapters:validate +``` diff --git a/docs/PRODUCTION_PLAN/phase-2-permissions.md b/docs/PRODUCTION_PLAN/phase-2-permissions.md new file mode 100644 index 0000000..e64483c --- /dev/null +++ b/docs/PRODUCTION_PLAN/phase-2-permissions.md @@ -0,0 +1,24 @@ +# Phase 2 — Permission dedup + +## Goal + +Single `intersectStructuredPermissions` in `packages/core/src/permissions.ts` (or dedicated module). + +## Files + +- New or extended `packages/core/src/permissions.ts` +- `packages/core/src/controller.ts` (remove duplicate) +- `packages/core/src/tool-broker.ts` (import shared) +- Unit tests for intersection edge cases + +## Exit criteria + +- PRODUCTION_GATE M1 +- No behavior change (refactor only) + +## Verify + +```bash +rg "function intersectStructuredPermissions" packages/core/src +npm test -- packages/core +``` diff --git a/docs/PRODUCTION_PLAN/phase-3-controller.md b/docs/PRODUCTION_PLAN/phase-3-controller.md new file mode 100644 index 0000000..8109dff --- /dev/null +++ b/docs/PRODUCTION_PLAN/phase-3-controller.md @@ -0,0 +1,27 @@ +# Phase 3 — Controller split + +## Goal + +Extract validators from `packages/core/src/controller.ts` without changing public API. + +## PR stack + +| PR | Extract | Target file | +|----|---------|-------------| +| 3a | Evidence validation | `controller/evidence-validator.ts` | +| 3b | Cross-artifact validation | `controller/cross-artifact-validator.ts` | +| 3c | Work-plan + permission trace helpers | `controller/work-plan-validator.ts`, `controller/permission-trace.ts` | + +Facade remains `RunController` in `controller/index.ts` or slimmed `controller.ts`. + +## Exit criteria + +- PRODUCTION_GATE M2, M3 +- `full-flow-conformance.test.ts` passes + +## Verify + +```bash +wc -l packages/core/src/controller.ts +npm test -- packages/core test/full-flow-conformance.test.ts +``` diff --git a/docs/PRODUCTION_PLAN/phase-4-ledger.md b/docs/PRODUCTION_PLAN/phase-4-ledger.md new file mode 100644 index 0000000..671d9ce --- /dev/null +++ b/docs/PRODUCTION_PLAN/phase-4-ledger.md @@ -0,0 +1,21 @@ +# Phase 4 — Ledger concurrency + +## Goal + +Fix flaky multi-worker ledger tests; make concurrency suite reliable in CI. + +## Files + +- `packages/core/src/ledger.test.ts` +- `packages/core/src/sqlite-ledger.ts` (if race fix needed) + +## Exit criteria + +- PRODUCTION_GATE Q4 + +## Verify + +```bash +npm test -- packages/core/src/ledger.test.ts +npm test -- --coverage +``` diff --git a/docs/PRODUCTION_PLAN/phase-5-storage.md b/docs/PRODUCTION_PLAN/phase-5-storage.md new file mode 100644 index 0000000..0d2edf8 --- /dev/null +++ b/docs/PRODUCTION_PLAN/phase-5-storage.md @@ -0,0 +1,25 @@ +# Phase 5 — Storage lifecycle + +## Goal + +`telic purge-run RUN_ID` and `telic gc` for orphan blob cleanup. + +## Files + +- `packages/cli/src/index.ts` +- New `packages/cli/src/purge-run.ts`, `packages/cli/src/gc.ts` +- Tests with `TELIC_STATE_DIR` +- `docs/API.md` + +## Exit criteria + +- Commands documented and tested +- Dry-run mode for `gc` + +## Verify + +```bash +npm test -- packages/cli +telic purge-run --help +telic gc --help +``` diff --git a/docs/PRODUCTION_PLAN/phase-6-release.md b/docs/PRODUCTION_PLAN/phase-6-release.md new file mode 100644 index 0000000..568ea0e --- /dev/null +++ b/docs/PRODUCTION_PLAN/phase-6-release.md @@ -0,0 +1,20 @@ +# Phase 6 — Release v0.2.0 + +## Goal + +Run [PRODUCTION_GATE.md](../PRODUCTION_GATE.md), tag, ship. + +## Steps + +1. Walk every gate row; record pass/fail. +2. Bump version to `0.2.0` in workspace packages and `telic-mcp`. +3. Update `CHANGELOG.md`. +4. `/ship` → merge to `main`. +5. Tag `v0.2.0`. +6. `/land-and-deploy` + `/monitor` for website if applicable. +7. `/document-release`. + +## Exit criteria + +- All PRODUCTION_GATE rows pass +- `git tag -l v0.2.0` From 88afad84f7fe9c55bd60efa935704532fc5bb238 Mon Sep 17 00:00:00 2001 From: Dukeabadoon Date: Wed, 12 Aug 2026 13:05:46 +0800 Subject: [PATCH 2/7] style: format production plan docs --- README.md | 1 - docs/PRODUCTION_GATE.md | 54 +++++++++++----------- docs/PRODUCTION_PLAN/overview.md | 18 ++++---- docs/PRODUCTION_PLAN/phase-3-controller.md | 10 ++-- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index fabda0a..6df7000 100644 --- a/README.md +++ b/README.md @@ -371,4 +371,3 @@ reproducible, non-sensitive defects. ## License Telic is released under the [MIT License](LICENSE). - diff --git a/docs/PRODUCTION_GATE.md b/docs/PRODUCTION_GATE.md index eef81a8..bfe5e52 100644 --- a/docs/PRODUCTION_GATE.md +++ b/docs/PRODUCTION_GATE.md @@ -6,45 +6,45 @@ Checklist for declaring **v0.2.0** production-ready preview. Every row must pass ## Trust boundary -| # | Criterion | Verify | -|---|-----------|--------| -| T1 | Broker hooks fail-closed when `TELIC_BROKER_STRICT=1` (default in adapter hooks) | `npm test -- packages/cli/src/broker-gate.test.ts test/cagt-broker-hook-e2e.test.ts` | -| T2 | Hook denies when `telic` CLI is missing (strict) | E2E or hook unit test | -| T3 | README and STATUS state Telic does not cage the host without hooks | Manual read | +| # | Criterion | Verify | +| --- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| T1 | Broker hooks fail-closed when `TELIC_BROKER_STRICT=1` (default in adapter hooks) | `npm test -- packages/cli/src/broker-gate.test.ts test/cagt-broker-hook-e2e.test.ts` | +| T2 | Hook denies when `telic` CLI is missing (strict) | E2E or hook unit test | +| T3 | README and STATUS state Telic does not cage the host without hooks | Manual read | ## Maintainability -| # | Criterion | Verify | -|---|-----------|--------| -| M1 | `intersectStructuredPermissions` has one implementation | `rg intersectStructuredPermissions packages/core/src` shows one definition | -| M2 | `controller.ts` facade under 1,500 lines; validators extracted | `wc -l packages/core/src/controller.ts` | -| M3 | Extracted validators have unit tests | `ls packages/core/src/controller/*.test.ts` or `validators/` | +| # | Criterion | Verify | +| --- | -------------------------------------------------------------- | -------------------------------------------------------------------------- | +| M1 | `intersectStructuredPermissions` has one implementation | `rg intersectStructuredPermissions packages/core/src` shows one definition | +| M2 | `controller.ts` facade under 1,500 lines; validators extracted | `wc -l packages/core/src/controller.ts` | +| M3 | Extracted validators have unit tests | `ls packages/core/src/controller/*.test.ts` or `validators/` | ## Quality -| # | Criterion | Verify | -|---|-----------|--------| -| Q1 | Full test suite passes | `npm test` | -| Q2 | Adapter handshakes pass | `npm run adapters:validate` | -| Q3 | Site builds (if web touched) | `npm run site:check` | -| Q4 | Coverage run stable (ledger concurrency) | `npm test -- --coverage` three consecutive green runs | +| # | Criterion | Verify | +| --- | ---------------------------------------- | ----------------------------------------------------- | +| Q1 | Full test suite passes | `npm test` | +| Q2 | Adapter handshakes pass | `npm run adapters:validate` | +| Q3 | Site builds (if web touched) | `npm run site:check` | +| Q4 | Coverage run stable (ledger concurrency) | `npm test -- --coverage` three consecutive green runs | ## Review -| # | Criterion | Verify | -|---|-----------|--------| -| R1 | `/review` or equivalent on full diff vs `main` | PR review notes | -| R2 | Bugbot triaged (fix or dismiss with reason) | PR comments | -| R3 | Security review on broker + permissions (Phase 1) | security-review artifact | +| # | Criterion | Verify | +| --- | ------------------------------------------------- | ------------------------ | +| R1 | `/review` or equivalent on full diff vs `main` | PR review notes | +| R2 | Bugbot triaged (fix or dismiss with reason) | PR comments | +| R3 | Security review on broker + permissions (Phase 1) | security-review artifact | ## Release -| # | Criterion | Verify | -|---|-----------|--------| -| S1 | `CHANGELOG.md` updated for v0.2.0 | File diff | -| S2 | Workspace + `telic-mcp` version `0.2.0` | `package.json` | -| S3 | Git tag `v0.2.0` on `main` | `git tag -l v0.2.0` | -| S4 | `main` CI green after merge | `gh pr checks` or Actions | +| # | Criterion | Verify | +| --- | --------------------------------------- | ------------------------- | +| S1 | `CHANGELOG.md` updated for v0.2.0 | File diff | +| S2 | Workspace + `telic-mcp` version `0.2.0` | `package.json` | +| S3 | Git tag `v0.2.0` on `main` | `git tag -l v0.2.0` | +| S4 | `main` CI green after merge | `gh pr checks` or Actions | ## Explicitly out of scope for v0.2.0 diff --git a/docs/PRODUCTION_PLAN/overview.md b/docs/PRODUCTION_PLAN/overview.md index 52d1152..d51f59a 100644 --- a/docs/PRODUCTION_PLAN/overview.md +++ b/docs/PRODUCTION_PLAN/overview.md @@ -16,15 +16,15 @@ System review graded protocol and tests highly (S), but security posture (B) and ## Phases -| Phase | Doc | Branch | Playbook | -|-------|-----|--------|----------| -| 0 | [phase-0-docs.md](./phase-0-docs.md) | `feat/production-phase-0` | multi-phase-plan | -| 1 | [phase-1-broker.md](./phase-1-broker.md) | `feat/production-phase-1-broker` | feature | -| 2 | [phase-2-permissions.md](./phase-2-permissions.md) | `feat/production-phase-2-permissions` | refactoring | -| 3 | [phase-3-controller.md](./phase-3-controller.md) | `feat/production-phase-3a` … `3c` | refactoring | -| 4 | [phase-4-ledger.md](./phase-4-ledger.md) | `feat/production-phase-4-ledger` | bug-fix | -| 5 | [phase-5-storage.md](./phase-5-storage.md) | `feat/production-phase-5-storage` | feature | -| 6 | [phase-6-release.md](./phase-6-release.md) | `release/v0.2.0` | shipping | +| Phase | Doc | Branch | Playbook | +| ----- | -------------------------------------------------- | ------------------------------------- | ---------------- | +| 0 | [phase-0-docs.md](./phase-0-docs.md) | `feat/production-phase-0` | multi-phase-plan | +| 1 | [phase-1-broker.md](./phase-1-broker.md) | `feat/production-phase-1-broker` | feature | +| 2 | [phase-2-permissions.md](./phase-2-permissions.md) | `feat/production-phase-2-permissions` | refactoring | +| 3 | [phase-3-controller.md](./phase-3-controller.md) | `feat/production-phase-3a` … `3c` | refactoring | +| 4 | [phase-4-ledger.md](./phase-4-ledger.md) | `feat/production-phase-4-ledger` | bug-fix | +| 5 | [phase-5-storage.md](./phase-5-storage.md) | `feat/production-phase-5-storage` | feature | +| 6 | [phase-6-release.md](./phase-6-release.md) | `release/v0.2.0` | shipping | ## Per-phase gate diff --git a/docs/PRODUCTION_PLAN/phase-3-controller.md b/docs/PRODUCTION_PLAN/phase-3-controller.md index 8109dff..d16fac6 100644 --- a/docs/PRODUCTION_PLAN/phase-3-controller.md +++ b/docs/PRODUCTION_PLAN/phase-3-controller.md @@ -6,11 +6,11 @@ Extract validators from `packages/core/src/controller.ts` without changing publi ## PR stack -| PR | Extract | Target file | -|----|---------|-------------| -| 3a | Evidence validation | `controller/evidence-validator.ts` | -| 3b | Cross-artifact validation | `controller/cross-artifact-validator.ts` | -| 3c | Work-plan + permission trace helpers | `controller/work-plan-validator.ts`, `controller/permission-trace.ts` | +| PR | Extract | Target file | +| --- | ------------------------------------ | --------------------------------------------------------------------- | +| 3a | Evidence validation | `controller/evidence-validator.ts` | +| 3b | Cross-artifact validation | `controller/cross-artifact-validator.ts` | +| 3c | Work-plan + permission trace helpers | `controller/work-plan-validator.ts`, `controller/permission-trace.ts` | Facade remains `RunController` in `controller/index.ts` or slimmed `controller.ts`. From e754caf173b952705a7ddc0161911b379d93c258 Mon Sep 17 00:00:00 2001 From: Dukeabadoon Date: Wed, 12 Aug 2026 13:06:32 +0800 Subject: [PATCH 3/7] feat(cli): fail-closed broker-gate in strict mode Default adapter hooks to TELIC_BROKER_STRICT=1, deny when the CLI is missing, and deny mapped mutating tools when Telic cannot evaluate an active run. --- .../project/.cline/hooks/broker-gate.mjs | 18 ++++++- .../project/.cursor/hooks/broker-gate.mjs | 18 ++++++- .../project/.roo/hooks/broker-gate.mjs | 18 ++++++- docs/STATUS.md | 5 +- packages/cli/src/broker-gate.test.ts | 52 ++++++++++++++---- packages/cli/src/broker-gate.ts | 54 ++++++++++++++++--- plugins/telic/hooks/broker-gate.mjs | 18 ++++++- 7 files changed, 159 insertions(+), 24 deletions(-) diff --git a/adapters/cline/project/.cline/hooks/broker-gate.mjs b/adapters/cline/project/.cline/hooks/broker-gate.mjs index f1ee864..516dc36 100644 --- a/adapters/cline/project/.cline/hooks/broker-gate.mjs +++ b/adapters/cline/project/.cline/hooks/broker-gate.mjs @@ -4,14 +4,30 @@ import { spawnSync } from "node:child_process"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.TELIC_BROKER_STRICT ??= "1"; + const stdin = readFileSync(0, "utf8"); const repositoryRoot = process.cwd(); +function denyCliUnavailable() { + process.stdout.write( + JSON.stringify({ + permission: "deny", + user_message: + "Telic broker-gate is unavailable in strict mode; install telic-mcp or set TELIC_BROKER_PERMISSIVE=1 for local development only.", + agent_message: + "Telic broker-gate CLI is missing. Do not bypass this tool call without Telic permission evaluation.", + }), + ); + process.exit(0); +} + function tryBrokerGate(command, args) { const result = spawnSync(command, args, { input: stdin, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], + env: process.env, }); if (result.error?.code === "ENOENT") return false; process.stdout.write(result.stdout ?? ""); @@ -41,4 +57,4 @@ for (const bin of [ } } -process.stdout.write(JSON.stringify({ permission: "allow" })); +denyCliUnavailable(); diff --git a/adapters/cursor/project/.cursor/hooks/broker-gate.mjs b/adapters/cursor/project/.cursor/hooks/broker-gate.mjs index f1ee864..516dc36 100644 --- a/adapters/cursor/project/.cursor/hooks/broker-gate.mjs +++ b/adapters/cursor/project/.cursor/hooks/broker-gate.mjs @@ -4,14 +4,30 @@ import { spawnSync } from "node:child_process"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.TELIC_BROKER_STRICT ??= "1"; + const stdin = readFileSync(0, "utf8"); const repositoryRoot = process.cwd(); +function denyCliUnavailable() { + process.stdout.write( + JSON.stringify({ + permission: "deny", + user_message: + "Telic broker-gate is unavailable in strict mode; install telic-mcp or set TELIC_BROKER_PERMISSIVE=1 for local development only.", + agent_message: + "Telic broker-gate CLI is missing. Do not bypass this tool call without Telic permission evaluation.", + }), + ); + process.exit(0); +} + function tryBrokerGate(command, args) { const result = spawnSync(command, args, { input: stdin, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], + env: process.env, }); if (result.error?.code === "ENOENT") return false; process.stdout.write(result.stdout ?? ""); @@ -41,4 +57,4 @@ for (const bin of [ } } -process.stdout.write(JSON.stringify({ permission: "allow" })); +denyCliUnavailable(); diff --git a/adapters/roo-code/project/.roo/hooks/broker-gate.mjs b/adapters/roo-code/project/.roo/hooks/broker-gate.mjs index f1ee864..516dc36 100644 --- a/adapters/roo-code/project/.roo/hooks/broker-gate.mjs +++ b/adapters/roo-code/project/.roo/hooks/broker-gate.mjs @@ -4,14 +4,30 @@ import { spawnSync } from "node:child_process"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.TELIC_BROKER_STRICT ??= "1"; + const stdin = readFileSync(0, "utf8"); const repositoryRoot = process.cwd(); +function denyCliUnavailable() { + process.stdout.write( + JSON.stringify({ + permission: "deny", + user_message: + "Telic broker-gate is unavailable in strict mode; install telic-mcp or set TELIC_BROKER_PERMISSIVE=1 for local development only.", + agent_message: + "Telic broker-gate CLI is missing. Do not bypass this tool call without Telic permission evaluation.", + }), + ); + process.exit(0); +} + function tryBrokerGate(command, args) { const result = spawnSync(command, args, { input: stdin, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], + env: process.env, }); if (result.error?.code === "ENOENT") return false; process.stdout.write(result.stdout ?? ""); @@ -41,4 +57,4 @@ for (const bin of [ } } -process.stdout.write(JSON.stringify({ permission: "allow" })); +denyCliUnavailable(); diff --git a/docs/STATUS.md b/docs/STATUS.md index c044f33..767afc2 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -11,7 +11,7 @@ Telic is an executable, local source preview. It is suitable for development and | Protocol | Strict Zod v4 schemas for controller, intent, serial execution, evidence, release, and trace artifacts; canonical bodies use camelCase and `schemaVersion: "1.0"` | | Controller | Deterministic phase ordering, stage-aware verification preflight, one user-facing clarification, one contract revision, one shared remediation, and terminal reports | | Topology (CAGT) | `micro`, `standard`, and `forensic` classification at `startRun`; EGEL promotes insufficient micro evidence to standard instead of terminal block | -| Tool broker | `telic_check_tool_action` plus preview `telic broker-gate` hooks for Cursor, Cline, and Roo; host-native calls outside MCP remain unintercepted | +| Tool broker | `telic_check_tool_action` plus preview `telic broker-gate` hooks for Cursor, Cline, and Roo (`TELIC_BROKER_STRICT=1` by default in hooks); host-native calls outside MCP remain unintercepted | | Forensic replay | `telic replay` CLI and `telic_replay_run` MCP inspector with digest verification; micro topology returns a degraded replay flag | | Persistence | SQLite metadata/events plus immutable SHA-256-addressed JSON bodies; digest verification occurs on read | | Context | Bounded inventory; token-boundary path ranking; eight-file zero-score fallback cap; relevance/file/byte budgets; path, symlink, duplicate, and heuristic secret controls | @@ -52,7 +52,7 @@ Current controls include strict schemas, bounded inputs, missing-reference rejec Important limits: -- **Host-native actions are mostly not intercepted.** If a host uses its own shell, editor, browser, or repository tool directly, Telic is not in that call path unless preview broker-gate hooks are installed (Cursor, Cline, Roo). Even then, prevention still depends on host sandboxing, approvals, and adapter compliance. +- **Host-native actions are mostly not intercepted.** If a host uses its own shell, editor, browser, or repository tool directly, Telic is not in that call path unless preview broker-gate hooks are installed (Cursor, Cline, Roo). With hooks enabled, strict mode denies mapped mutating tools when Telic cannot evaluate an active run. Set `TELIC_BROKER_PERMISSIVE=1` only for local development. Prevention still depends on host sandboxing, approvals, and adapter compliance. - **Same-user state is not an adversarial vault.** SHA-256 and SQLite consistency detect ordinary corruption and mismatches. A malicious process with the same OS account and filesystem access may be able to replace metadata and blobs together. Use OS permissions and an isolated account/workspace for stronger separation. - **Secret scanning is heuristic.** It can miss uncommon credentials and can exclude harmless text. Do not ground repositories containing secrets you are unwilling to store locally, and do not treat the context selector as a dedicated secret scanner. - **Exact local artifacts may be sensitive.** Selected source and submitted evidence are stored exactly in the content-addressed store. Hashing is identity/integrity metadata, not anonymization. @@ -99,3 +99,4 @@ automated CI suite. Native Windows and WSL are not in the current test matrix. 5. Complete the dependency and security review, then publish a monitored vulnerability-reporting channel. 6. Publish a pinned artifact and record its checksum and repository URL. + diff --git a/packages/cli/src/broker-gate.test.ts b/packages/cli/src/broker-gate.test.ts index 123fb49..8f95cc3 100644 --- a/packages/cli/src/broker-gate.test.ts +++ b/packages/cli/src/broker-gate.test.ts @@ -1,25 +1,55 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; -import { evaluateBrokerGate, mapHookInputToToolCall } from "./broker-gate.js"; +import { evaluateBrokerGate, isBrokerStrict, mapHookInputToToolCall } from "./broker-gate.js"; + +const writeHook = { + tool_name: "Write", + tool_input: { path: "src/foo.ts" }, +}; describe("broker gate", () => { + const envSnapshot = { ...process.env }; + + afterEach(() => { + process.env = { ...envSnapshot }; + }); + it("maps write tools to repository.write", () => { + expect(mapHookInputToToolCall(writeHook)).toEqual({ + capability: "repository.write", + target: "src/foo.ts", + }); + }); + + it("fails open without an active session when not strict", () => { + delete process.env.TELIC_BROKER_STRICT; + delete process.env.TELIC_BROKER_PERMISSIVE; expect( - mapHookInputToToolCall({ - tool_name: "Write", - tool_input: { path: "src/foo.ts" }, + evaluateBrokerGate({ + repositoryRoot: process.cwd(), + hookInput: writeHook, }), - ).toEqual({ capability: "repository.write", target: "src/foo.ts" }); + ).toEqual({ permission: "allow" }); + }); + + it("fails closed without an active session when strict", () => { + process.env.TELIC_BROKER_STRICT = "1"; + const result = evaluateBrokerGate({ + repositoryRoot: process.cwd(), + hookInput: writeHook, + }); + expect(result.permission).toBe("deny"); + expect(result.user_message).toContain("strict mode"); }); - it("fails open without an active session", () => { + it("respects permissive override", () => { + process.env.TELIC_BROKER_STRICT = "1"; + process.env.TELIC_BROKER_PERMISSIVE = "1"; + expect(isBrokerStrict()).toBe(false); expect( evaluateBrokerGate({ repositoryRoot: process.cwd(), - hookInput: { - tool_name: "Write", - tool_input: { path: "src/foo.ts" }, - }, + hookInput: writeHook, }), ).toEqual({ permission: "allow" }); }); diff --git a/packages/cli/src/broker-gate.ts b/packages/cli/src/broker-gate.ts index fc2228d..debf7d2 100644 --- a/packages/cli/src/broker-gate.ts +++ b/packages/cli/src/broker-gate.ts @@ -27,6 +27,24 @@ function asRecord(value: unknown): Record | null { : null; } +export function isBrokerStrict(): boolean { + if (process.env.TELIC_BROKER_PERMISSIVE === "1") { + return false; + } + return process.env.TELIC_BROKER_STRICT === "1"; +} + +function denyStrict( + userMessage: string, + agentMessage: string, +): HookPermissionResponse { + return { + permission: "deny", + user_message: userMessage, + agent_message: agentMessage, + }; +} + export function mapHookInputToToolCall( hookInput: Record, ): { capability: string; target?: string } | null { @@ -60,20 +78,36 @@ export function mapHookInputToToolCall( export function evaluateBrokerGate( request: BrokerGateRequest, ): HookPermissionResponse { + const strict = isBrokerStrict(); const repositoryRoot = realpathSync(resolve(request.repositoryRoot)); const stateDirectory = process.env.TELIC_STATE_DIR ? resolve(process.env.TELIC_STATE_DIR) : defaultStateDirectory(repositoryRoot); + const mapped = mapHookInputToToolCall(request.hookInput); const session = readActiveSession(stateDirectory); - if (!session || session.repositoryRoot !== repositoryRoot) { + + if (!mapped) { return { permission: "allow" }; } - if (!existsSync(resolve(stateDirectory, "ledger.sqlite3"))) { - return { permission: "allow" }; + + if (!session || session.repositoryRoot !== repositoryRoot) { + if (!strict) { + return { permission: "allow" }; + } + return denyStrict( + "Telic broker is in strict mode but no active Telic session matches this repository.", + "Start or resume a Telic run before mutating tools, or set TELIC_BROKER_PERMISSIVE=1 for local development only.", + ); } - const mapped = mapHookInputToToolCall(request.hookInput); - if (!mapped) { - return { permission: "allow" }; + + if (!existsSync(resolve(stateDirectory, "ledger.sqlite3"))) { + if (!strict) { + return { permission: "allow" }; + } + return denyStrict( + "Telic broker is in strict mode but no ledger exists for this repository.", + "Run telic doctor and ensure Telic state is initialized before mutating tools.", + ); } const ledger = new SqliteLedger(stateDirectory); @@ -84,7 +118,13 @@ export function evaluateBrokerGate( run.status !== "running" || run.version !== session.runVersion ) { - return { permission: "allow" }; + if (!strict) { + return { permission: "allow" }; + } + return denyStrict( + "Telic broker is in strict mode but the active session run is not running.", + "Resume or start a Telic run before mutating tools, or set TELIC_BROKER_PERMISSIVE=1 for local development only.", + ); } const envelopeRecord = ledger.findLatestArtifact( session.runId, diff --git a/plugins/telic/hooks/broker-gate.mjs b/plugins/telic/hooks/broker-gate.mjs index f1ee864..516dc36 100644 --- a/plugins/telic/hooks/broker-gate.mjs +++ b/plugins/telic/hooks/broker-gate.mjs @@ -4,14 +4,30 @@ import { spawnSync } from "node:child_process"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +process.env.TELIC_BROKER_STRICT ??= "1"; + const stdin = readFileSync(0, "utf8"); const repositoryRoot = process.cwd(); +function denyCliUnavailable() { + process.stdout.write( + JSON.stringify({ + permission: "deny", + user_message: + "Telic broker-gate is unavailable in strict mode; install telic-mcp or set TELIC_BROKER_PERMISSIVE=1 for local development only.", + agent_message: + "Telic broker-gate CLI is missing. Do not bypass this tool call without Telic permission evaluation.", + }), + ); + process.exit(0); +} + function tryBrokerGate(command, args) { const result = spawnSync(command, args, { input: stdin, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], + env: process.env, }); if (result.error?.code === "ENOENT") return false; process.stdout.write(result.stdout ?? ""); @@ -41,4 +57,4 @@ for (const bin of [ } } -process.stdout.write(JSON.stringify({ permission: "allow" })); +denyCliUnavailable(); From 56ca00ae9b5902cf8101f58dc26bd7f88bdcdb5c Mon Sep 17 00:00:00 2001 From: Dukeabadoon Date: Wed, 12 Aug 2026 13:10:30 +0800 Subject: [PATCH 4/7] style: fix prettier for CI format check --- apps/web/components/copy-button.tsx | 1 - apps/web/package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/web/components/copy-button.tsx b/apps/web/components/copy-button.tsx index d90d4c8..cd96a54 100644 --- a/apps/web/components/copy-button.tsx +++ b/apps/web/components/copy-button.tsx @@ -33,4 +33,3 @@ export function CopyButton({ ); } - diff --git a/apps/web/package.json b/apps/web/package.json index 638fecf..b7e24e1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,4 +30,3 @@ "node": ">=24" } } - From d82408f26d842c059e822d986b2963ef32c16822 Mon Sep 17 00:00:00 2001 From: Dukeabadoon Date: Wed, 12 Aug 2026 13:10:30 +0800 Subject: [PATCH 5/7] style: fix prettier for CI format check --- docs/STATUS.md | 29 ++++++++++++++-------------- packages/cli/src/broker-gate.test.ts | 6 +++++- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/STATUS.md b/docs/STATUS.md index 767afc2..f9110ac 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -6,20 +6,20 @@ Telic is an executable, local source preview. It is suitable for development and ## Current vertical slice -| Area | Current behavior | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Protocol | Strict Zod v4 schemas for controller, intent, serial execution, evidence, release, and trace artifacts; canonical bodies use camelCase and `schemaVersion: "1.0"` | -| Controller | Deterministic phase ordering, stage-aware verification preflight, one user-facing clarification, one contract revision, one shared remediation, and terminal reports | -| Topology (CAGT) | `micro`, `standard`, and `forensic` classification at `startRun`; EGEL promotes insufficient micro evidence to standard instead of terminal block | -| Tool broker | `telic_check_tool_action` plus preview `telic broker-gate` hooks for Cursor, Cline, and Roo (`TELIC_BROKER_STRICT=1` by default in hooks); host-native calls outside MCP remain unintercepted | -| Forensic replay | `telic replay` CLI and `telic_replay_run` MCP inspector with digest verification; micro topology returns a degraded replay flag | -| Persistence | SQLite metadata/events plus immutable SHA-256-addressed JSON bodies; digest verification occurs on read | -| Context | Bounded inventory; token-boundary path ranking; eight-file zero-score fallback cap; relevance/file/byte budgets; path, symlink, duplicate, and heuristic secret controls | -| MCP | Local STDIO server with eleven tools plus a host-neutral `telic_workflow` prompt | -| CLI | npm-packaged and source-built `doctor`, `status`, `trace`, `artifact`, `replay`, `broker-gate`, and `mcp` commands | -| Host package | Codex reference plugin plus seven experimental source packs generated from one canonical skill and MCP bundle | -| Model access | None in the runtime; the active host model authors semantic artifacts | -| Network service | None required; normal transport is local STDIO | +| Area | Current behavior | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Protocol | Strict Zod v4 schemas for controller, intent, serial execution, evidence, release, and trace artifacts; canonical bodies use camelCase and `schemaVersion: "1.0"` | +| Controller | Deterministic phase ordering, stage-aware verification preflight, one user-facing clarification, one contract revision, one shared remediation, and terminal reports | +| Topology (CAGT) | `micro`, `standard`, and `forensic` classification at `startRun`; EGEL promotes insufficient micro evidence to standard instead of terminal block | +| Tool broker | `telic_check_tool_action` plus preview `telic broker-gate` hooks for Cursor, Cline, and Roo (`TELIC_BROKER_STRICT=1` by default in hooks); host-native calls outside MCP remain unintercepted | +| Forensic replay | `telic replay` CLI and `telic_replay_run` MCP inspector with digest verification; micro topology returns a degraded replay flag | +| Persistence | SQLite metadata/events plus immutable SHA-256-addressed JSON bodies; digest verification occurs on read | +| Context | Bounded inventory; token-boundary path ranking; eight-file zero-score fallback cap; relevance/file/byte budgets; path, symlink, duplicate, and heuristic secret controls | +| MCP | Local STDIO server with eleven tools plus a host-neutral `telic_workflow` prompt | +| CLI | npm-packaged and source-built `doctor`, `status`, `trace`, `artifact`, `replay`, `broker-gate`, and `mcp` commands | +| Host package | Codex reference plugin plus seven experimental source packs generated from one canonical skill and MCP bundle | +| Model access | None in the runtime; the active host model authors semantic artifacts | +| Network service | None required; normal transport is local STDIO | Automated checks cover protocol fixtures/invariants, controller transitions, permissions, ledger behavior, context selection/security controls, MCP service/tools, CLI behavior, an end-to-end artifact pipeline, and the standalone plugin handshake. The current verification commands are listed in [Installation](INSTALLATION.md); test totals are intentionally not frozen in prose. @@ -99,4 +99,3 @@ automated CI suite. Native Windows and WSL are not in the current test matrix. 5. Complete the dependency and security review, then publish a monitored vulnerability-reporting channel. 6. Publish a pinned artifact and record its checksum and repository URL. - diff --git a/packages/cli/src/broker-gate.test.ts b/packages/cli/src/broker-gate.test.ts index 8f95cc3..02a3126 100644 --- a/packages/cli/src/broker-gate.test.ts +++ b/packages/cli/src/broker-gate.test.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it } from "vitest"; -import { evaluateBrokerGate, isBrokerStrict, mapHookInputToToolCall } from "./broker-gate.js"; +import { + evaluateBrokerGate, + isBrokerStrict, + mapHookInputToToolCall, +} from "./broker-gate.js"; const writeHook = { tool_name: "Write", From 35ec93c58e5005afbe136b0751676723a9c07668 Mon Sep 17 00:00:00 2001 From: Dukeabadoon Date: Wed, 12 Aug 2026 13:28:44 +0800 Subject: [PATCH 6/7] test: raise vitest timeout for Windows CI Conformance harness tests exceed 5s on windows-latest runners. --- packages/core/src/controller.ts | 64 +-------------------------- packages/core/src/permissions.test.ts | 45 +++++++++++++++++++ packages/core/src/permissions.ts | 62 ++++++++++++++++++++++++++ packages/core/src/tool-broker.ts | 64 +-------------------------- vitest.config.ts | 2 + 5 files changed, 113 insertions(+), 124 deletions(-) diff --git a/packages/core/src/controller.ts b/packages/core/src/controller.ts index f8391a1..e7a86ff 100644 --- a/packages/core/src/controller.ts +++ b/packages/core/src/controller.ts @@ -5,6 +5,8 @@ import { canonicalJson } from "./canonical-json.js"; import { SqliteLedger } from "./ledger.js"; import { authorizeAction, + emptyPermissionSet, + intersectStructuredPermissions, normalizeNetworkReadDomain, permissionSetIsSubset, policyForMode, @@ -159,17 +161,6 @@ const MAX_TOOL_CALLS_PER_RUN = 4_000; const REFERENCE_URI_PATTERN = /^(?:artifact|trace|repo):\/\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$/u; -function emptyPermissionSet(): StructuredPermissionSet { - return { - repository: { read: [], write: [], delete: [] }, - shell: { inspect: false, executeAllowlist: [] }, - runtime: { inspect: [], restart: [] }, - browser: { inspect: false, mutateState: false }, - network: { readDomains: [], externalWrite: false }, - subagents: { spawn: false, maximumChildren: 0, maximumDepth: 0 }, - }; -} - function capabilitiesToPermissionSet( capabilities: string[], shellExecuteAllowlist: string[] = [], @@ -228,57 +219,6 @@ function capabilitiesToPermissionSet( return permissions; } -function intersectStructuredPermissions( - projection: ReturnType, - granted: StructuredPermissionSet, - denied: StructuredPermissionSet, -): StructuredPermissionSet { - const effective = emptyPermissionSet(); - if (projection.repository_read && denied.repository.read.length === 0) { - effective.repository.read = [...granted.repository.read]; - } - if (projection.repository_write && denied.repository.write.length === 0) { - effective.repository.write = [...granted.repository.write]; - } - if (projection.repository_delete && denied.repository.delete.length === 0) { - effective.repository.delete = [...granted.repository.delete]; - } - if (projection.shell_execute && denied.shell.executeAllowlist.length === 0) { - effective.shell.executeAllowlist = [...granted.shell.executeAllowlist]; - } - effective.shell.inspect = - projection.shell_inspect && granted.shell.inspect && !denied.shell.inspect; - if (projection.runtime_inspect && denied.runtime.inspect.length === 0) { - effective.runtime.inspect = [...granted.runtime.inspect]; - } - if (projection.runtime_mutate && denied.runtime.restart.length === 0) { - effective.runtime.restart = [...granted.runtime.restart]; - } - effective.browser.inspect = - projection.browser_inspect && - granted.browser.inspect && - !denied.browser.inspect; - effective.browser.mutateState = - projection.browser_mutate && - granted.browser.mutateState && - !denied.browser.mutateState; - if (projection.network_read && denied.network.readDomains.length === 0) { - effective.network.readDomains = [...granted.network.readDomains]; - } - effective.network.externalWrite = - projection.external_write && - granted.network.externalWrite && - !denied.network.externalWrite; - if ( - projection.subagent_spawn && - granted.subagents.spawn && - !denied.subagents.spawn - ) { - effective.subagents = { ...granted.subagents }; - } - return effective; -} - function explicitPermissionProjection( mode: IntentMode, envelope: unknown, diff --git a/packages/core/src/permissions.test.ts b/packages/core/src/permissions.test.ts index 941222a..a8c1a9b 100644 --- a/packages/core/src/permissions.test.ts +++ b/packages/core/src/permissions.test.ts @@ -6,6 +6,8 @@ import { describe, expect, it } from "vitest"; import { authorizeAction, + emptyPermissionSet, + intersectStructuredPermissions, permissionSetIsSubset, policyForMode, projectPermissions, @@ -231,3 +233,46 @@ describe("permission intersection", () => { } }); }); + +describe("intersectStructuredPermissions", () => { + it("returns empty when mode projection denies repository write", () => { + const projection = projectPermissions("analyze_only"); + const granted = emptyPermissionSet(); + granted.repository.write = ["**"]; + const effective = intersectStructuredPermissions( + projection, + granted, + emptyPermissionSet(), + ); + expect(effective.repository.write).toEqual([]); + }); + + it("drops granted scopes when denied lists are non-empty", () => { + const projection = projectPermissions("analyze_and_fix"); + const granted = emptyPermissionSet(); + granted.repository.read = ["src/**"]; + const denied = emptyPermissionSet(); + denied.repository.read = ["infra/**"]; + const effective = intersectStructuredPermissions( + projection, + granted, + denied, + ); + expect(effective.repository.read).toEqual([]); + }); + + it("preserves subagent limits only when spawn is allowed and not denied", () => { + const projection = projectPermissions("analyze_and_fix"); + const granted = emptyPermissionSet(); + granted.subagents = { spawn: true, maximumChildren: 3, maximumDepth: 1 }; + const denied = emptyPermissionSet(); + denied.subagents.spawn = true; + expect( + intersectStructuredPermissions(projection, granted, denied).subagents, + ).toEqual(emptyPermissionSet().subagents); + expect( + intersectStructuredPermissions(projection, granted, emptyPermissionSet()) + .subagents, + ).toEqual(granted.subagents); + }); +}); diff --git a/packages/core/src/permissions.ts b/packages/core/src/permissions.ts index cdf646e..bc426d0 100644 --- a/packages/core/src/permissions.ts +++ b/packages/core/src/permissions.ts @@ -352,3 +352,65 @@ export function projectPermissions(mode: IntentMode): PermissionProjection { subagent_spawn: modeAllows(mode, "subagent.spawn"), }; } + +export function emptyPermissionSet(): StructuredPermissionSet { + return { + repository: { read: [], write: [], delete: [] }, + shell: { inspect: false, executeAllowlist: [] }, + runtime: { inspect: [], restart: [] }, + browser: { inspect: false, mutateState: false }, + network: { readDomains: [], externalWrite: false }, + subagents: { spawn: false, maximumChildren: 0, maximumDepth: 0 }, + }; +} + +export function intersectStructuredPermissions( + projection: PermissionProjection, + granted: StructuredPermissionSet, + denied: StructuredPermissionSet, +): StructuredPermissionSet { + const effective = emptyPermissionSet(); + if (projection.repository_read && denied.repository.read.length === 0) { + effective.repository.read = [...granted.repository.read]; + } + if (projection.repository_write && denied.repository.write.length === 0) { + effective.repository.write = [...granted.repository.write]; + } + if (projection.repository_delete && denied.repository.delete.length === 0) { + effective.repository.delete = [...granted.repository.delete]; + } + if (projection.shell_execute && denied.shell.executeAllowlist.length === 0) { + effective.shell.executeAllowlist = [...granted.shell.executeAllowlist]; + } + effective.shell.inspect = + projection.shell_inspect && granted.shell.inspect && !denied.shell.inspect; + if (projection.runtime_inspect && denied.runtime.inspect.length === 0) { + effective.runtime.inspect = [...granted.runtime.inspect]; + } + if (projection.runtime_mutate && denied.runtime.restart.length === 0) { + effective.runtime.restart = [...granted.runtime.restart]; + } + effective.browser.inspect = + projection.browser_inspect && + granted.browser.inspect && + !denied.browser.inspect; + effective.browser.mutateState = + projection.browser_mutate && + granted.browser.mutateState && + !denied.browser.mutateState; + if (projection.network_read && denied.network.readDomains.length === 0) { + effective.network.readDomains = [...granted.network.readDomains]; + } + effective.network.externalWrite = + projection.external_write && + granted.network.externalWrite && + !denied.network.externalWrite; + if ( + projection.subagent_spawn && + granted.subagents.spawn && + !denied.subagents.spawn + ) { + effective.subagents = { ...granted.subagents }; + } + return effective; +} diff --git a/packages/core/src/tool-broker.ts b/packages/core/src/tool-broker.ts index c32f2a0..7f0a4d1 100644 --- a/packages/core/src/tool-broker.ts +++ b/packages/core/src/tool-broker.ts @@ -1,5 +1,7 @@ import { authorizeAction, + emptyPermissionSet, + intersectStructuredPermissions, normalizeNetworkReadDomain, policyForMode, policyFromPermissionSet, @@ -43,68 +45,6 @@ export interface BrokerContext { policyRefs?: string[]; } -function emptyPermissionSet(): StructuredPermissionSet { - return { - repository: { read: [], write: [], delete: [] }, - shell: { inspect: false, executeAllowlist: [] }, - runtime: { inspect: [], restart: [] }, - browser: { inspect: false, mutateState: false }, - network: { readDomains: [], externalWrite: false }, - subagents: { spawn: false, maximumChildren: 0, maximumDepth: 0 }, - }; -} - -function intersectStructuredPermissions( - projection: ReturnType, - granted: StructuredPermissionSet, - denied: StructuredPermissionSet, -): StructuredPermissionSet { - const effective = emptyPermissionSet(); - if (projection.repository_read && denied.repository.read.length === 0) { - effective.repository.read = [...granted.repository.read]; - } - if (projection.repository_write && denied.repository.write.length === 0) { - effective.repository.write = [...granted.repository.write]; - } - if (projection.repository_delete && denied.repository.delete.length === 0) { - effective.repository.delete = [...granted.repository.delete]; - } - if (projection.shell_execute && denied.shell.executeAllowlist.length === 0) { - effective.shell.executeAllowlist = [...granted.shell.executeAllowlist]; - } - effective.shell.inspect = - projection.shell_inspect && granted.shell.inspect && !denied.shell.inspect; - if (projection.runtime_inspect && denied.runtime.inspect.length === 0) { - effective.runtime.inspect = [...granted.runtime.inspect]; - } - if (projection.runtime_mutate && denied.runtime.restart.length === 0) { - effective.runtime.restart = [...granted.runtime.restart]; - } - effective.browser.inspect = - projection.browser_inspect && - granted.browser.inspect && - !denied.browser.inspect; - effective.browser.mutateState = - projection.browser_mutate && - granted.browser.mutateState && - !denied.browser.mutateState; - if (projection.network_read && denied.network.readDomains.length === 0) { - effective.network.readDomains = [...granted.network.readDomains]; - } - effective.network.externalWrite = - projection.external_write && - granted.network.externalWrite && - !denied.network.externalWrite; - if ( - projection.subagent_spawn && - granted.subagents.spawn && - !denied.subagents.spawn - ) { - effective.subagents = { ...granted.subagents }; - } - return effective; -} - /** Derive effective permissions from a RunEnvelope authorization block. */ export function permissionsFromEnvelope( mode: IntentMode, diff --git a/vitest.config.ts b/vitest.config.ts index 859d5c1..00a059d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ }, test: { environment: "node", + testTimeout: 15_000, include: [ "packages/**/*.test.ts", "test/**/*.test.ts", @@ -45,3 +46,4 @@ export default defineConfig({ }, }, }); + From eeda49dd6c55652378df691527eb1d26f5df5e79 Mon Sep 17 00:00:00 2001 From: Dukeabadoon Date: Wed, 12 Aug 2026 13:32:22 +0800 Subject: [PATCH 7/7] style: format vitest.config.ts --- vitest.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index 00a059d..c1d27a7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -46,4 +46,3 @@ export default defineConfig({ }, }, }); -