@davidorex/pi-agent-dispatch 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +37 -0
  3. package/dist/attested-commit.d.ts +32 -0
  4. package/dist/attested-commit.d.ts.map +1 -0
  5. package/dist/attested-commit.js +61 -0
  6. package/dist/attested-commit.js.map +1 -0
  7. package/dist/auth-gate.d.ts +92 -0
  8. package/dist/auth-gate.d.ts.map +1 -0
  9. package/dist/auth-gate.js +210 -0
  10. package/dist/auth-gate.js.map +1 -0
  11. package/dist/author-agent-spec-tool.d.ts +33 -0
  12. package/dist/author-agent-spec-tool.d.ts.map +1 -0
  13. package/dist/author-agent-spec-tool.js +98 -0
  14. package/dist/author-agent-spec-tool.js.map +1 -0
  15. package/dist/author-tool-grant-tool.d.ts +47 -0
  16. package/dist/author-tool-grant-tool.d.ts.map +1 -0
  17. package/dist/author-tool-grant-tool.js +87 -0
  18. package/dist/author-tool-grant-tool.js.map +1 -0
  19. package/dist/call-agent-tool.d.ts +42 -0
  20. package/dist/call-agent-tool.d.ts.map +1 -0
  21. package/dist/call-agent-tool.js +90 -0
  22. package/dist/call-agent-tool.js.map +1 -0
  23. package/dist/capability-composer.d.ts +11 -0
  24. package/dist/capability-composer.d.ts.map +1 -0
  25. package/dist/capability-composer.js +35 -0
  26. package/dist/capability-composer.js.map +1 -0
  27. package/dist/commit-attested-tool.d.ts +29 -0
  28. package/dist/commit-attested-tool.d.ts.map +1 -0
  29. package/dist/commit-attested-tool.js +45 -0
  30. package/dist/commit-attested-tool.js.map +1 -0
  31. package/dist/composite-loader.d.ts +36 -0
  32. package/dist/composite-loader.d.ts.map +1 -0
  33. package/dist/composite-loader.js +137 -0
  34. package/dist/composite-loader.js.map +1 -0
  35. package/dist/composites/command-allowlist.d.ts +29 -0
  36. package/dist/composites/command-allowlist.d.ts.map +1 -0
  37. package/dist/composites/command-allowlist.js +36 -0
  38. package/dist/composites/command-allowlist.js.map +1 -0
  39. package/dist/composites/git-log.d.ts +31 -0
  40. package/dist/composites/git-log.d.ts.map +1 -0
  41. package/dist/composites/git-log.js +39 -0
  42. package/dist/composites/git-log.js.map +1 -0
  43. package/dist/composites/grep-paths.d.ts +26 -0
  44. package/dist/composites/grep-paths.d.ts.map +1 -0
  45. package/dist/composites/grep-paths.js +34 -0
  46. package/dist/composites/grep-paths.js.map +1 -0
  47. package/dist/composites/read-files.d.ts +24 -0
  48. package/dist/composites/read-files.d.ts.map +1 -0
  49. package/dist/composites/read-files.js +35 -0
  50. package/dist/composites/read-files.js.map +1 -0
  51. package/dist/index.d.ts +18 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +77 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/operation-vocab.d.ts +25 -0
  56. package/dist/operation-vocab.d.ts.map +1 -0
  57. package/dist/operation-vocab.js +78 -0
  58. package/dist/operation-vocab.js.map +1 -0
  59. package/dist/read-truncation-gate.d.ts +143 -0
  60. package/dist/read-truncation-gate.d.ts.map +1 -0
  61. package/dist/read-truncation-gate.js +175 -0
  62. package/dist/read-truncation-gate.js.map +1 -0
  63. package/dist/real-check-runner.d.ts +66 -0
  64. package/dist/real-check-runner.d.ts.map +1 -0
  65. package/dist/real-check-runner.js +133 -0
  66. package/dist/real-check-runner.js.map +1 -0
  67. package/dist/run-real-checks-tool.d.ts +28 -0
  68. package/dist/run-real-checks-tool.d.ts.map +1 -0
  69. package/dist/run-real-checks-tool.js +47 -0
  70. package/dist/run-real-checks-tool.js.map +1 -0
  71. package/dist/run-work-order-loop-tool.d.ts +35 -0
  72. package/dist/run-work-order-loop-tool.d.ts.map +1 -0
  73. package/dist/run-work-order-loop-tool.js +46 -0
  74. package/dist/run-work-order-loop-tool.js.map +1 -0
  75. package/dist/verified-identity.d.ts +54 -0
  76. package/dist/verified-identity.d.ts.map +1 -0
  77. package/dist/verified-identity.js +133 -0
  78. package/dist/verified-identity.js.map +1 -0
  79. package/dist/work-order-loop.d.ts +82 -0
  80. package/dist/work-order-loop.d.ts.map +1 -0
  81. package/dist/work-order-loop.js +149 -0
  82. package/dist/work-order-loop.js.map +1 -0
  83. package/package.json +59 -0
  84. package/skill-narrative.md +53 -0
  85. package/skills/pi-agent-dispatch/SKILL.md +138 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/). This package has not yet been published to npm; the accumulated surface below — from its scaffold (`a1d8072`, 2026-05-28) onward — sits under `[Unreleased]` and becomes its first published version's section at first publish.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.28.0] - 2026-06-03
8
+
9
+ ### Added
10
+ - Extension scaffold: operation-vocab + capability-composer; `author-agent-spec`, `call-agent`, and `run-real-checks` Pi tools with their dispatch/enforcement wiring (`a1d8072`, `5b84aff`, `8532c44`, `d568980`, `b0f4983`, `e1a64bd`, `c2d50e1`)
11
+ - `commit-attested` Pi tool with `attested-commit` (writer.kind=agent attestation, husky-as-backup) (`d3df262`, `bbf7179`)
12
+ - Bounded composite-capability vocabulary: `read-files`, `git-log`, `grep-paths`, and `command-allowlist` composite KIND libraries; `FORBIDDEN_WHOLESALE_OPERATIONS` L1 invariant; composite-loader registering dynamic Pi tools from `config.tool_operations[]`; `author-tool-grant` Pi tool with L1∪L5 forbidden-operation enforcement and an L3 runtime guard (`e2fffc0`, `0afa1b4`, `98b382a`, `69ca081`, `e1ad57b`, `30ccfc0`, `ed9f66f`, `0256323`)
13
+ - `run-work-order-loop` Pi tool over a work-order-loop library (bounded iteration + human-OK gate at the boundary) (`494357a`, `7c94352`)
14
+ - `TraceEntry` `extension_load_warning` entry kind + composite-loader config-null observability (`8030c61`)
15
+ - Auth-gate: per-tool user-auth library invoked via `pi.on('tool_call')`, wired into the extension factory to intercept Bucket-2 tools regardless of caller-supplied `writer.kind`; verified-identity module (reads git config `user.email` with `USER` env fallback) that mutates `event.input.writer` to the confirmed terminal-operator identity; `context-switch` + `context-archive` added to the auth-required set (`7be299b`, `e6a1799`, `d872fce`, `3bbb555`, `f27aafe`)
16
+ - Read-truncation-gate: library plus `pi.on('tool_result')` handler that replaces truncated read content with a structured hard-refusal directive at the pi-dispatch layer (`38ad48e`, `7a09a27`)
17
+
18
+ ### Changed
19
+ - Folded auth gating into per-package registry-derived gated-sets (`45dcf66`)
20
+ - Renamed the 'human-only' framing to the canonical 'human-authorized via auth-gate confirm' model across docstrings/error-messages; stripped redundant in-body `writer.kind=='human'` checks from three tools now that the auth-gate is the canonical identity check; routed `write-schema-migration` through the auth-gate (`fc67993`, `f939777`, `bbad988`)
21
+ - Wholesale rewrite of README + skill-narrative + scrubbed `src/*.ts` description strings (`2422bad`)
22
+
23
+ ### Fixed
24
+ - Isolated child git env from inherited `GIT_DIR`/`GIT_*` (hook leak) (`8c3b4cf`)
25
+ - Corrected the read-truncation-gate directive logic for the `firstLineExceedsLimit`, `lastLinePartial`, and `truncatedBy='bytes'` `TruncationResult` variants that previously produced operationally-misguiding output (`b20ccd4`)
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @davidorex/pi-agent-dispatch
2
+
3
+ In-pi agent-as-tool dispatch + capability composition + the bounded north-star work-order loop. Sibling Pi extension to pi-context / pi-workflows / pi-behavior-monitors; consumes pi-jit-agents as a library (no separate extension registration for the agent runtime).
4
+
5
+ ## Boundary
6
+
7
+ This package is the sub-agent agent-as-tool registration site for the harness-confined orchestrator. The orchestrator's positive clause — substrate-write + call-agent + author-agent-spec + run-real-checks + commit-attested + author-tool-grant + run-work-order-loop + declared composites — fires through tools registered here. The negative clause forbids the orchestrator from running bash / edit / write directly; capability widening is gated by writer.kind=human authoring.
8
+
9
+ ## Public Pi tools (6 static + dynamic composites)
10
+
11
+ | Tool | Purpose |
12
+ |------|---------|
13
+ | `call-agent` | Dispatch a declared agent spec with a composed grant (parent ∩ requested ∩ spec.tools). |
14
+ | `author-agent-spec` | Write an `.agent.yaml` spec to the substrate. writer.kind=human enforced. |
15
+ | `author-tool-grant` | Add/remove `config.tool_operations[]` entries. Refuses non-human writers + forbidden-wholesale tokens (bash/edit/write) + L1 ∪ L5 forbidden union violations. |
16
+ | `run-real-checks` | Execute declared build/check/test + runtime-demo + adversarial-probe checks for a work-order. Verdict is the actual exit code per the deterministic-real-check governance — never an LLM self-report. |
17
+ | `commit-attested` | Stage + commit declared files with `Attested-by: agent/<id>` + `Work-order: <id>` footer. Refuses on missing agent_id / files / message. |
18
+ | `run-work-order-loop` | Single-call wrapper for the bounded north-star loop: dispatch target_agent → run-real-checks → on-pass commit-attested → on-fail human-OK retry. Bounded iterations (default 3). |
19
+
20
+ In addition, `composite-loader` reads the active substrate's `config.tool_operations[]` on extension load and dynamically registers each declared bounded composite as a Pi tool. Forbidden tokens in the L1 (framework wholesale) ∪ L5 (project-declared) forbidden union are refused. Config-absent loads degrade quietly: the 6 static tools remain available; the absence is surfaced via the `extension_load_warning` TraceEntry.
21
+
22
+ ## Capability composition
23
+
24
+ Tool grants are operation-granular. Defaults are EMPTY. At each dispatch the grant is composed as `parent ∩ requested`; at the runtime boundary the JIT-runtime clamp enforces `child ⊆ parent`. The `FORBIDDEN_WHOLESALE_OPERATIONS` set rejects shipping wholesale L1 surfaces as a single composite token; L5 project-declared forbidden tokens add to the union. Capability widening goes through `author-tool-grant` with writer.kind=human, never agent / monitor / workflow.
25
+
26
+ ## Work-order loop closure
27
+
28
+ The `run-work-order-loop` tool consolidates the orchestrator's prior per-iteration chain (call-agent → run-real-checks → on-pass commit-attested → on-fail decide-to-retry) into one Pi call. Every gate the prior chain enforced — capability composition at the call boundary, deterministic real-check verdict, writer-attestation footer, human-OK retry at the iteration boundary via `ctx.ui.confirm` — fires from inside the wrapped library. No path bypasses them.
29
+
30
+ ## Canonical rules
31
+
32
+ - Harness-confined orchestrator (positive + negative clauses).
33
+ - Sibling-consumer scope; pi-jit-agents stays a library.
34
+ - writer.kind=human authoring; default-empty grants; terminal verdict = real deterministic checks the executive cannot fake.
35
+ - Capability composition + end-to-end work-order loop + bounded-composite vocabulary + launch-chain integration.
36
+ - Orchestrator uses jit-agents directly (no wrapping extension).
37
+ - Capability composition lives in the dispatch layer.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * attested-commit — stage declared files + invoke `git commit` with a
3
+ * DispatchContext-style attestation footer encoding writer.kind=agent
4
+ * per DEC-0047. The husky pre-commit hook (`npm run check && npm test`)
5
+ * runs as the backup gate; never bypass via --no-verify. The primary
6
+ * gate is run-real-checks (TASK-090) called BEFORE this tool.
7
+ *
8
+ * Refusal gates fire before any git op: missing agent_id, empty files
9
+ * array, or empty message throws CommitAttestedRefusedError. Stages
10
+ * files one-by-one (`git add <file>` per call) to avoid the canon-
11
+ * violating `git add -A` / `git add .` shape. On commit, captures
12
+ * exit_code + stdout + stderr; when committed, captures the short SHA
13
+ * via `git log -1 --format=%h`.
14
+ */
15
+ export interface AttestedCommitOptions {
16
+ files: string[];
17
+ message: string;
18
+ agent_id: string;
19
+ work_order_id?: string;
20
+ }
21
+ export interface AttestedCommitResult {
22
+ committed: boolean;
23
+ commit_sha?: string;
24
+ exit_code: number;
25
+ stdout: string;
26
+ stderr: string;
27
+ }
28
+ export declare class CommitAttestedRefusedError extends Error {
29
+ constructor(reason: string);
30
+ }
31
+ export declare function attestedCommit(cwd: string, options: AttestedCommitOptions): Promise<AttestedCommitResult>;
32
+ //# sourceMappingURL=attested-commit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attested-commit.d.ts","sourceRoot":"","sources":["../src/attested-commit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,MAAM,WAAW,qBAAqB;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,0BAA2B,SAAQ,KAAK;gBACxC,MAAM,EAAE,MAAM;CAI1B;AAQD,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAuC/G"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * attested-commit — stage declared files + invoke `git commit` with a
3
+ * DispatchContext-style attestation footer encoding writer.kind=agent
4
+ * per DEC-0047. The husky pre-commit hook (`npm run check && npm test`)
5
+ * runs as the backup gate; never bypass via --no-verify. The primary
6
+ * gate is run-real-checks (TASK-090) called BEFORE this tool.
7
+ *
8
+ * Refusal gates fire before any git op: missing agent_id, empty files
9
+ * array, or empty message throws CommitAttestedRefusedError. Stages
10
+ * files one-by-one (`git add <file>` per call) to avoid the canon-
11
+ * violating `git add -A` / `git add .` shape. On commit, captures
12
+ * exit_code + stdout + stderr; when committed, captures the short SHA
13
+ * via `git log -1 --format=%h`.
14
+ */
15
+ import { spawnSync } from "node:child_process";
16
+ import { cleanGitEnv } from "@davidorex/pi-context/git-env";
17
+ export class CommitAttestedRefusedError extends Error {
18
+ constructor(reason) {
19
+ super(`commit-attested: ${reason}`);
20
+ this.name = "CommitAttestedRefusedError";
21
+ }
22
+ }
23
+ function composeMessage(message, agent_id, work_order_id) {
24
+ const lines = [message.trimEnd(), "", `Attested-by: agent/${agent_id}`];
25
+ if (work_order_id)
26
+ lines.push(`Work-order: ${work_order_id}`);
27
+ return `${lines.join("\n")}\n`;
28
+ }
29
+ export async function attestedCommit(cwd, options) {
30
+ if (!options.agent_id || options.agent_id.trim() === "") {
31
+ throw new CommitAttestedRefusedError("agent_id is required to construct the 'Attested-by: agent/<id>' commit footer");
32
+ }
33
+ if (!options.files || options.files.length === 0) {
34
+ throw new CommitAttestedRefusedError("files[] is required (at least one staged file)");
35
+ }
36
+ if (!options.message || options.message.trim() === "") {
37
+ throw new CommitAttestedRefusedError("message is required");
38
+ }
39
+ // per-file `git add` — never `git add -A` / `git add .` per canon
40
+ for (const file of options.files) {
41
+ const addResult = spawnSync("git", ["add", file], { cwd, encoding: "utf-8", env: cleanGitEnv() });
42
+ if (addResult.status !== 0) {
43
+ throw new CommitAttestedRefusedError(`git add '${file}' failed (exit ${addResult.status}): ${addResult.stderr || addResult.stdout}`);
44
+ }
45
+ }
46
+ const composed = composeMessage(options.message, options.agent_id, options.work_order_id);
47
+ const commitResult = spawnSync("git", ["commit", "-m", composed], { cwd, encoding: "utf-8", env: cleanGitEnv() });
48
+ const exit_code = commitResult.status ?? 1;
49
+ const stdout = commitResult.stdout ?? "";
50
+ const stderr = commitResult.stderr ?? "";
51
+ const committed = exit_code === 0;
52
+ let commit_sha;
53
+ if (committed) {
54
+ const shaResult = spawnSync("git", ["log", "-1", "--format=%h"], { cwd, encoding: "utf-8", env: cleanGitEnv() });
55
+ if (shaResult.status === 0) {
56
+ commit_sha = (shaResult.stdout ?? "").trim() || undefined;
57
+ }
58
+ }
59
+ return { committed, commit_sha, exit_code, stdout, stderr };
60
+ }
61
+ //# sourceMappingURL=attested-commit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attested-commit.js","sourceRoot":"","sources":["../src/attested-commit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * auth-gate — per-tool user-authorization handler registered on
3
+ * `pi.on("tool_call", ...)` from the pi-agent-dispatch extension factory.
4
+ *
5
+ * Canonical model: the pi.on('tool_call') boundary is the structural
6
+ * identity check for sensitive substrate-write surfaces. Tools like
7
+ * author-agent-spec / commit-attested / author-tool-grant are
8
+ * human-authorized via auth-gate confirm — the agent may issue the call,
9
+ * the operator authorizes at the terminal, and on confirm=true the
10
+ * handler stamps event.input.writer with a verified terminal-operator
11
+ * identity (overriding whatever the caller supplied). Caller-supplied
12
+ * writer.kind fields are not trusted as the identity check; the
13
+ * pi-dispatch event fires regardless of the tool's execute() body,
14
+ * regardless of which extension registered the tool, and regardless of
15
+ * caller-supplied argument shapes. Returning `{ block: true, reason }`
16
+ * prevents execution entirely.
17
+ *
18
+ * Surface: the canonical Bucket-2 tools declared in `AUTH_REQUIRED_TOOLS`,
19
+ * which is the aggregation of four per-package gated-sets (see below).
20
+ * The handler enforces:
21
+ * - non-interactive context (ctx.hasUI === false) → unconditional
22
+ * refusal with a structured reason naming the missing interactivity.
23
+ * This closes the JSON-mode / workflow-subprocess bypass: a step
24
+ * that auto-invokes a Bucket-2 tool without an attached operator
25
+ * cannot proceed.
26
+ * - interactive context (ctx.hasUI === true) → ctx.ui.confirm(title,
27
+ * message) where message renders the tool name + sanitized arg
28
+ * summary; on operator decline returns block:true; on accept the
29
+ * handler mutates event.input.writer to the verified-operator
30
+ * identity (when discoverable) then returns void (allow). Tool
31
+ * bodies subsequently read the mutated input and persist the
32
+ * verified identity through DispatchContext stamping.
33
+ *
34
+ * Non-Bucket-2 tools (read-block, call-agent, run-real-checks, the SDK
35
+ * built-ins bash/read/edit/write/grep/find/ls, dynamic composite tools,
36
+ * etc.) pass through unconditionally — the gate is narrowly targeted at
37
+ * the sensitive-substrate-write surface.
38
+ *
39
+ * Co-existence: pi-behavior-monitors registers its own tool_call handler
40
+ * (packages/pi-behavior-monitors/index.ts) for the deviation-monitor
41
+ * pipeline. Per pi-coding-agent's documented multi-handler semantics
42
+ * (types.d.ts:809 — the `on` registration is additive, not exclusive),
43
+ * both handlers coexist; pi-dispatch invokes each in registration order
44
+ * and honors the first `block:true` returned. No ordering conflict
45
+ * expected — pi-behavior-monitors' handler operates on classification
46
+ * verdicts; this gate operates on toolName allowlist.
47
+ */
48
+ import type { ExtensionAPI, ExtensionContext, ToolCallEvent, ToolCallEventResult } from "@earendil-works/pi-coding-agent";
49
+ /**
50
+ * The canonical Bucket-2 tool names whose execution requires an affirmative
51
+ * user-confirm, assembled as the aggregation of four per-package gated-sets —
52
+ * each set OWNED BY (co-located with) the package whose tools it gates:
53
+ * - pi-agent-dispatch → dispatchGatedTools (defined above)
54
+ * - pi-context → @davidorex/pi-context/ops `gatedTools` (derived from
55
+ * the op-registry's `authGated` flags)
56
+ * - pi-workflows → @davidorex/pi-workflows/auth-required `gatedTools`
57
+ * - pi-behavior-monitors → @davidorex/pi-behavior-monitors/auth-required
58
+ * `gatedTools`
59
+ *
60
+ * Sourcing membership from each package keeps the gate's allowlist in step with
61
+ * the surfaces it gates: a package that adds or removes a sensitive tool changes
62
+ * its own owned set, and this aggregation reflects it without an edit here. The
63
+ * substrate-persisted schema-version migration declaration surface
64
+ * (write-schema-migration) is among pi-context's gated set because
65
+ * capability/migration authoring is on the human-authorized authorization path.
66
+ *
67
+ * Vocabulary changes still require a source edit + release in the owning
68
+ * package per the canonical human-authorized governance model; the gate does
69
+ * not read mutable runtime state to decide what is gated.
70
+ */
71
+ export declare const AUTH_REQUIRED_TOOLS: readonly string[];
72
+ /**
73
+ * The pi.on('tool_call') handler. Exported separately from the
74
+ * registration helper so unit tests can invoke it directly with a mock
75
+ * event + mock context, without driving a real pi extension factory.
76
+ *
77
+ * Returns:
78
+ * - `void` when the tool is not in AUTH_REQUIRED_TOOLS (pass through);
79
+ * - `void` when the tool is in AUTH_REQUIRED_TOOLS, ctx.hasUI is true,
80
+ * and ctx.ui.confirm resolves true (operator allowed);
81
+ * - `{ block: true, reason }` when ctx.hasUI is false (non-interactive
82
+ * refusal) or when ctx.ui.confirm resolves false (operator declined).
83
+ */
84
+ export declare function authGateHandler(event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined>;
85
+ /**
86
+ * Register the auth-gate handler on a pi extension API. Single line at
87
+ * the factory call-site. Idempotent registration is the responsibility
88
+ * of the caller (the extension factory runs once per pi process; double-
89
+ * registration would produce duplicate prompts).
90
+ */
91
+ export declare function registerAuthGate(pi: ExtensionAPI): void;
92
+ //# sourceMappingURL=auth-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-gate.d.ts","sourceRoot":"","sources":["../src/auth-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAMH,OAAO,KAAK,EACX,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,MAAM,iCAAiC,CAAC;AAWzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAKhD,CAAC;AAwCF;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACpC,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,gBAAgB,GACnB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CA4D1C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAEvD"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * auth-gate — per-tool user-authorization handler registered on
3
+ * `pi.on("tool_call", ...)` from the pi-agent-dispatch extension factory.
4
+ *
5
+ * Canonical model: the pi.on('tool_call') boundary is the structural
6
+ * identity check for sensitive substrate-write surfaces. Tools like
7
+ * author-agent-spec / commit-attested / author-tool-grant are
8
+ * human-authorized via auth-gate confirm — the agent may issue the call,
9
+ * the operator authorizes at the terminal, and on confirm=true the
10
+ * handler stamps event.input.writer with a verified terminal-operator
11
+ * identity (overriding whatever the caller supplied). Caller-supplied
12
+ * writer.kind fields are not trusted as the identity check; the
13
+ * pi-dispatch event fires regardless of the tool's execute() body,
14
+ * regardless of which extension registered the tool, and regardless of
15
+ * caller-supplied argument shapes. Returning `{ block: true, reason }`
16
+ * prevents execution entirely.
17
+ *
18
+ * Surface: the canonical Bucket-2 tools declared in `AUTH_REQUIRED_TOOLS`,
19
+ * which is the aggregation of four per-package gated-sets (see below).
20
+ * The handler enforces:
21
+ * - non-interactive context (ctx.hasUI === false) → unconditional
22
+ * refusal with a structured reason naming the missing interactivity.
23
+ * This closes the JSON-mode / workflow-subprocess bypass: a step
24
+ * that auto-invokes a Bucket-2 tool without an attached operator
25
+ * cannot proceed.
26
+ * - interactive context (ctx.hasUI === true) → ctx.ui.confirm(title,
27
+ * message) where message renders the tool name + sanitized arg
28
+ * summary; on operator decline returns block:true; on accept the
29
+ * handler mutates event.input.writer to the verified-operator
30
+ * identity (when discoverable) then returns void (allow). Tool
31
+ * bodies subsequently read the mutated input and persist the
32
+ * verified identity through DispatchContext stamping.
33
+ *
34
+ * Non-Bucket-2 tools (read-block, call-agent, run-real-checks, the SDK
35
+ * built-ins bash/read/edit/write/grep/find/ls, dynamic composite tools,
36
+ * etc.) pass through unconditionally — the gate is narrowly targeted at
37
+ * the sensitive-substrate-write surface.
38
+ *
39
+ * Co-existence: pi-behavior-monitors registers its own tool_call handler
40
+ * (packages/pi-behavior-monitors/index.ts) for the deviation-monitor
41
+ * pipeline. Per pi-coding-agent's documented multi-handler semantics
42
+ * (types.d.ts:809 — the `on` registration is additive, not exclusive),
43
+ * both handlers coexist; pi-dispatch invokes each in registration order
44
+ * and honors the first `block:true` returned. No ordering conflict
45
+ * expected — pi-behavior-monitors' handler operates on classification
46
+ * verdicts; this gate operates on toolName allowlist.
47
+ */
48
+ import { gatedTools as monitorsGatedTools } from "@davidorex/pi-behavior-monitors/auth-required";
49
+ import { describeIdentityOverride } from "@davidorex/pi-context/block-api";
50
+ import { gatedTools as contextGatedTools } from "@davidorex/pi-context/ops";
51
+ import { gatedTools as workflowsGatedTools } from "@davidorex/pi-workflows/auth-required";
52
+ import { getVerifiedOperatorIdentity } from "./verified-identity.js";
53
+ /**
54
+ * The pi-agent-dispatch-owned set of tool names whose execution requires an
55
+ * affirmative user-confirm. Co-located with the package that registers these
56
+ * tools (author-agent-spec / author-tool-grant / commit-attested are all
57
+ * pi-agent-dispatch surfaces) so membership travels with the surface it gates.
58
+ */
59
+ const dispatchGatedTools = ["author-agent-spec", "author-tool-grant", "commit-attested"];
60
+ /**
61
+ * The canonical Bucket-2 tool names whose execution requires an affirmative
62
+ * user-confirm, assembled as the aggregation of four per-package gated-sets —
63
+ * each set OWNED BY (co-located with) the package whose tools it gates:
64
+ * - pi-agent-dispatch → dispatchGatedTools (defined above)
65
+ * - pi-context → @davidorex/pi-context/ops `gatedTools` (derived from
66
+ * the op-registry's `authGated` flags)
67
+ * - pi-workflows → @davidorex/pi-workflows/auth-required `gatedTools`
68
+ * - pi-behavior-monitors → @davidorex/pi-behavior-monitors/auth-required
69
+ * `gatedTools`
70
+ *
71
+ * Sourcing membership from each package keeps the gate's allowlist in step with
72
+ * the surfaces it gates: a package that adds or removes a sensitive tool changes
73
+ * its own owned set, and this aggregation reflects it without an edit here. The
74
+ * substrate-persisted schema-version migration declaration surface
75
+ * (write-schema-migration) is among pi-context's gated set because
76
+ * capability/migration authoring is on the human-authorized authorization path.
77
+ *
78
+ * Vocabulary changes still require a source edit + release in the owning
79
+ * package per the canonical human-authorized governance model; the gate does
80
+ * not read mutable runtime state to decide what is gated.
81
+ */
82
+ export const AUTH_REQUIRED_TOOLS = [
83
+ ...dispatchGatedTools,
84
+ ...contextGatedTools,
85
+ ...workflowsGatedTools,
86
+ ...monitorsGatedTools,
87
+ ];
88
+ /**
89
+ * Summarize a tool-call argument object for operator-readable confirm
90
+ * prompts. Top-level keys are rendered; string values are truncated to
91
+ * ~80 chars + ellipsis; nested objects are rendered as `{...}`
92
+ * placeholders so secret-bearing structured args (e.g. spec bodies
93
+ * holding API keys, file contents passed by reference) never appear
94
+ * verbatim in the prompt.
95
+ *
96
+ * Deliberately non-exhaustive: this is an operator-readability aid,
97
+ * not a safe-render contract. Sensitive fields should never be passed
98
+ * by these tools in the first place; the summary is one final
99
+ * defensive truncation rather than the primary guard.
100
+ */
101
+ function summarizeArgs(input) {
102
+ if (!input || typeof input !== "object")
103
+ return "(no args)";
104
+ const keys = Object.keys(input);
105
+ if (keys.length === 0)
106
+ return "(no args)";
107
+ const parts = [];
108
+ for (const key of keys) {
109
+ const value = input[key];
110
+ if (value === null || value === undefined) {
111
+ parts.push(`${key}=${String(value)}`);
112
+ }
113
+ else if (typeof value === "string") {
114
+ const truncated = value.length > 80 ? `${value.slice(0, 80)}…` : value;
115
+ parts.push(`${key}="${truncated}"`);
116
+ }
117
+ else if (typeof value === "number" || typeof value === "boolean") {
118
+ parts.push(`${key}=${String(value)}`);
119
+ }
120
+ else if (Array.isArray(value)) {
121
+ parts.push(`${key}=[${value.length} item(s)]`);
122
+ }
123
+ else if (typeof value === "object") {
124
+ parts.push(`${key}={...}`);
125
+ }
126
+ else {
127
+ parts.push(`${key}=<${typeof value}>`);
128
+ }
129
+ }
130
+ return parts.join(", ");
131
+ }
132
+ /**
133
+ * The pi.on('tool_call') handler. Exported separately from the
134
+ * registration helper so unit tests can invoke it directly with a mock
135
+ * event + mock context, without driving a real pi extension factory.
136
+ *
137
+ * Returns:
138
+ * - `void` when the tool is not in AUTH_REQUIRED_TOOLS (pass through);
139
+ * - `void` when the tool is in AUTH_REQUIRED_TOOLS, ctx.hasUI is true,
140
+ * and ctx.ui.confirm resolves true (operator allowed);
141
+ * - `{ block: true, reason }` when ctx.hasUI is false (non-interactive
142
+ * refusal) or when ctx.ui.confirm resolves false (operator declined).
143
+ */
144
+ export async function authGateHandler(event, ctx) {
145
+ if (!AUTH_REQUIRED_TOOLS.includes(event.toolName)) {
146
+ return; // pass-through: tool is not in the gated set
147
+ }
148
+ if (!ctx.hasUI) {
149
+ return {
150
+ block: true,
151
+ reason: `tool ${event.toolName} requires interactive user-confirm; current context is non-interactive (ctx.hasUI=false)`,
152
+ };
153
+ }
154
+ const argSummary = summarizeArgs(event.input);
155
+ let message = `tool ${event.toolName} requested; args: ${argSummary}`;
156
+ // Informed-authorization (carried item 2): when the payload carries a schema
157
+ // (write-schema; write-schema-migration carries none) whose item subschema
158
+ // declares an `x-identity.metadata_fields` override, append a human delta so
159
+ // the operator confirms an INFORMED change to the content/metadata partition.
160
+ // When no override is declared (or no schema payload), the message is
161
+ // byte-identical to the pre-Cycle-3 form.
162
+ const rawSchema = event.input?.schema;
163
+ if (rawSchema !== undefined) {
164
+ let parsed = rawSchema;
165
+ if (typeof rawSchema === "string") {
166
+ try {
167
+ parsed = JSON.parse(rawSchema);
168
+ }
169
+ catch {
170
+ parsed = rawSchema; // non-JSON string → describeIdentityOverride returns null
171
+ }
172
+ }
173
+ const override = describeIdentityOverride(parsed);
174
+ if (override !== null) {
175
+ message = `${message}\nidentity metadata-field override declared:\n${override}\nmandatory floor id/oid/content_hash/content_parent remains excluded from the content hash.`;
176
+ }
177
+ }
178
+ const ok = await ctx.ui.confirm(`Authorize ${event.toolName}?`, message);
179
+ if (ok === false) {
180
+ return { block: true, reason: "user declined" };
181
+ }
182
+ // Canonical identity stamp: the auth-gate is the structural identity
183
+ // check, so once the operator has affirmed, the writer field on the
184
+ // pending tool input is overwritten with the verified terminal-
185
+ // operator identity. This mutates event.input in place per pi's
186
+ // documented tool_call mutation contract; downstream tool bodies read
187
+ // the mutated input and persist the verified identity through
188
+ // DispatchContext stamping. When no identity can be verified (both
189
+ // resolution sources absent — surfaced via a structured warning),
190
+ // caller-supplied writer is left untouched as a last-resort
191
+ // fall-through.
192
+ const verifiedIdentity = getVerifiedOperatorIdentity();
193
+ if (verifiedIdentity !== null) {
194
+ const input = event.input;
195
+ if (input && typeof input === "object") {
196
+ input.writer = { kind: "human", user: verifiedIdentity };
197
+ }
198
+ }
199
+ return;
200
+ }
201
+ /**
202
+ * Register the auth-gate handler on a pi extension API. Single line at
203
+ * the factory call-site. Idempotent registration is the responsibility
204
+ * of the caller (the extension factory runs once per pi process; double-
205
+ * registration would produce duplicate prompts).
206
+ */
207
+ export function registerAuthGate(pi) {
208
+ pi.on("tool_call", authGateHandler);
209
+ }
210
+ //# sourceMappingURL=auth-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-gate.js","sourceRoot":"","sources":["../src/auth-gate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * author-agent-spec Pi tool — writes .agent.yaml under the agents tier.
3
+ * Capability/spec authoring is human-authorized via the auth-gate confirm
4
+ * at the pi-dispatch layer: the agent may issue the call, the operator
5
+ * authorizes at the terminal, and the auth-gate stamps event.input.writer
6
+ * with the verified terminal-operator identity before the body runs. The
7
+ * body itself trusts the writer field as-is.
8
+ */
9
+ import { Type } from "@earendil-works/pi-ai";
10
+ import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ export declare const authorAgentSpecTool: {
12
+ name: string;
13
+ label: string;
14
+ description: string;
15
+ promptSnippet: string;
16
+ parameters: Type.TObject<{
17
+ name: Type.TString;
18
+ spec: Type.TUnknown;
19
+ writer: Type.TObject<{
20
+ kind: Type.TString;
21
+ user: Type.TString;
22
+ }>;
23
+ }>;
24
+ execute(_toolCallId: string, params: {
25
+ name: string;
26
+ spec: Record<string, unknown> | string;
27
+ writer: {
28
+ kind: string;
29
+ user: string;
30
+ };
31
+ }, _signal: AbortSignal, _onUpdate: AgentToolUpdateCallback, ctx: ExtensionContext): Promise<AgentToolResult<undefined>>;
32
+ };
33
+ //# sourceMappingURL=author-agent-spec-tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"author-agent-spec-tool.d.ts","sourceRoot":"","sources":["../src/author-agent-spec-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAGlH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;yBA8BjB,MAAM,UACX;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;QAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,WAC/F,WAAW,aACT,uBAAuB,OAC7B,gBAAgB,GACnB,OAAO,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;CAkEtC,CAAC"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * author-agent-spec Pi tool — writes .agent.yaml under the agents tier.
3
+ * Capability/spec authoring is human-authorized via the auth-gate confirm
4
+ * at the pi-dispatch layer: the agent may issue the call, the operator
5
+ * authorizes at the terminal, and the auth-gate stamps event.input.writer
6
+ * with the verified terminal-operator identity before the body runs. The
7
+ * body itself trusts the writer field as-is.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { tryResolveContextDir } from "@davidorex/pi-context/context-dir";
12
+ import { parseAgentYaml } from "@davidorex/pi-jit-agents/agent-spec";
13
+ import { Type } from "@earendil-works/pi-ai";
14
+ import { stringify as yamlStringify } from "yaml";
15
+ export const authorAgentSpecTool = {
16
+ name: "author-agent-spec",
17
+ label: "Author Agent Spec",
18
+ description: "Write a new .agent.yaml spec to the agents tier. Requires user authorization via interactive confirmation at the pi-dispatch auth-gate; on confirm, the verified terminal-operator identity is stamped as writer. The written file is AJV-validated against AgentSpec before persisting.",
19
+ promptSnippet: "Author a privileged JIT-agent spec — declares input, prompts, tools grant, output schema, contextBlocks.",
20
+ parameters: Type.Object({
21
+ name: Type.String({ description: "Agent name (becomes <name>.agent.yaml filename + AgentSpec.name)." }),
22
+ spec: Type.Unknown({
23
+ description: "AgentSpec object body (will be serialized to YAML). Must conform to AgentSpec shape.",
24
+ }),
25
+ writer: Type.Object({
26
+ kind: Type.String({
27
+ description: "Writer kind discriminator (human / agent / monitor / workflow). The pi-dispatch auth-gate overrides this with 'human' on confirm when a verified terminal-operator identity is discoverable.",
28
+ }),
29
+ user: Type.String({
30
+ description: "Writer identity (e.g. 'davidryan@gmail.com'). Overwritten by the auth-gate with the verified terminal-operator identity when one is discoverable on confirm=true.",
31
+ }),
32
+ }, {
33
+ description: "DispatchContext.writer payload; see pi-context/src/dispatch-context.ts for the discriminated union.",
34
+ }),
35
+ }),
36
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
37
+ // Identity check has moved to the pi-dispatch auth-gate
38
+ // (pi.on('tool_call') handler in this same package). By the time
39
+ // the execute body runs, the auth-gate has already prompted the
40
+ // operator and — on confirm=true with a verifiable identity —
41
+ // stamped event.input.writer with the verified terminal-operator
42
+ // identity. The body trusts the writer field as-is and uses
43
+ // writer.user to construct the DispatchContext for substrate
44
+ // stamping.
45
+ if (!params.writer?.user) {
46
+ throw new Error("author-agent-spec: writer.user is required.");
47
+ }
48
+ // Parse spec if string (defensive — Type.Unknown may arrive as JSON string)
49
+ let specObj = typeof params.spec === "string" ? null : params.spec;
50
+ if (typeof params.spec === "string") {
51
+ try {
52
+ specObj = JSON.parse(params.spec);
53
+ }
54
+ catch {
55
+ throw new Error("author-agent-spec: spec parameter must be an object, got unparseable string.");
56
+ }
57
+ }
58
+ // Resolve agents tier — substrate root + agents/ subdir
59
+ const root = tryResolveContextDir(ctx.cwd);
60
+ if (root === null) {
61
+ throw new Error("author-agent-spec: cannot resolve substrate context dir; .pi-context.json missing?");
62
+ }
63
+ const agentsDir = path.join(root, "agents");
64
+ if (!fs.existsSync(agentsDir))
65
+ fs.mkdirSync(agentsDir, { recursive: true });
66
+ const destPath = path.join(agentsDir, `${params.name}.agent.yaml`);
67
+ // Serialize + write atomically (tmp + rename)
68
+ const yamlContent = yamlStringify(specObj);
69
+ const tmpPath = `${destPath}.tmp-${process.pid}`;
70
+ fs.writeFileSync(tmpPath, yamlContent, "utf8");
71
+ try {
72
+ // Validate by round-tripping through parseAgentYaml
73
+ const parsed = parseAgentYaml(tmpPath);
74
+ // parseAgentYaml falls back to filename basename when name absent in YAML;
75
+ // only flag when spec carries an explicit name that disagrees with the tool param.
76
+ const explicitName = specObj.name;
77
+ if (typeof explicitName === "string" && explicitName !== params.name) {
78
+ throw new Error(`author-agent-spec: spec.name ('${explicitName}') mismatches tool param name ('${params.name}'). Align them.`);
79
+ }
80
+ void parsed;
81
+ }
82
+ catch (err) {
83
+ fs.unlinkSync(tmpPath);
84
+ throw err;
85
+ }
86
+ fs.renameSync(tmpPath, destPath);
87
+ return {
88
+ details: undefined,
89
+ content: [
90
+ {
91
+ type: "text",
92
+ text: `Wrote ${destPath} (writer=human:${params.writer.user})`,
93
+ },
94
+ ],
95
+ };
96
+ },
97
+ };
98
+ //# sourceMappingURL=author-agent-spec-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"author-agent-spec-tool.js","sourceRoot":"","sources":["../src/author-agent-spec-tool.ts"],"names":[],"mappings":"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"}