@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.
- package/CHANGELOG.md +25 -0
- package/README.md +37 -0
- package/dist/attested-commit.d.ts +32 -0
- package/dist/attested-commit.d.ts.map +1 -0
- package/dist/attested-commit.js +61 -0
- package/dist/attested-commit.js.map +1 -0
- package/dist/auth-gate.d.ts +92 -0
- package/dist/auth-gate.d.ts.map +1 -0
- package/dist/auth-gate.js +210 -0
- package/dist/auth-gate.js.map +1 -0
- package/dist/author-agent-spec-tool.d.ts +33 -0
- package/dist/author-agent-spec-tool.d.ts.map +1 -0
- package/dist/author-agent-spec-tool.js +98 -0
- package/dist/author-agent-spec-tool.js.map +1 -0
- package/dist/author-tool-grant-tool.d.ts +47 -0
- package/dist/author-tool-grant-tool.d.ts.map +1 -0
- package/dist/author-tool-grant-tool.js +87 -0
- package/dist/author-tool-grant-tool.js.map +1 -0
- package/dist/call-agent-tool.d.ts +42 -0
- package/dist/call-agent-tool.d.ts.map +1 -0
- package/dist/call-agent-tool.js +90 -0
- package/dist/call-agent-tool.js.map +1 -0
- package/dist/capability-composer.d.ts +11 -0
- package/dist/capability-composer.d.ts.map +1 -0
- package/dist/capability-composer.js +35 -0
- package/dist/capability-composer.js.map +1 -0
- package/dist/commit-attested-tool.d.ts +29 -0
- package/dist/commit-attested-tool.d.ts.map +1 -0
- package/dist/commit-attested-tool.js +45 -0
- package/dist/commit-attested-tool.js.map +1 -0
- package/dist/composite-loader.d.ts +36 -0
- package/dist/composite-loader.d.ts.map +1 -0
- package/dist/composite-loader.js +137 -0
- package/dist/composite-loader.js.map +1 -0
- package/dist/composites/command-allowlist.d.ts +29 -0
- package/dist/composites/command-allowlist.d.ts.map +1 -0
- package/dist/composites/command-allowlist.js +36 -0
- package/dist/composites/command-allowlist.js.map +1 -0
- package/dist/composites/git-log.d.ts +31 -0
- package/dist/composites/git-log.d.ts.map +1 -0
- package/dist/composites/git-log.js +39 -0
- package/dist/composites/git-log.js.map +1 -0
- package/dist/composites/grep-paths.d.ts +26 -0
- package/dist/composites/grep-paths.d.ts.map +1 -0
- package/dist/composites/grep-paths.js +34 -0
- package/dist/composites/grep-paths.js.map +1 -0
- package/dist/composites/read-files.d.ts +24 -0
- package/dist/composites/read-files.d.ts.map +1 -0
- package/dist/composites/read-files.js +35 -0
- package/dist/composites/read-files.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/operation-vocab.d.ts +25 -0
- package/dist/operation-vocab.d.ts.map +1 -0
- package/dist/operation-vocab.js +78 -0
- package/dist/operation-vocab.js.map +1 -0
- package/dist/read-truncation-gate.d.ts +143 -0
- package/dist/read-truncation-gate.d.ts.map +1 -0
- package/dist/read-truncation-gate.js +175 -0
- package/dist/read-truncation-gate.js.map +1 -0
- package/dist/real-check-runner.d.ts +66 -0
- package/dist/real-check-runner.d.ts.map +1 -0
- package/dist/real-check-runner.js +133 -0
- package/dist/real-check-runner.js.map +1 -0
- package/dist/run-real-checks-tool.d.ts +28 -0
- package/dist/run-real-checks-tool.d.ts.map +1 -0
- package/dist/run-real-checks-tool.js +47 -0
- package/dist/run-real-checks-tool.js.map +1 -0
- package/dist/run-work-order-loop-tool.d.ts +35 -0
- package/dist/run-work-order-loop-tool.d.ts.map +1 -0
- package/dist/run-work-order-loop-tool.js +46 -0
- package/dist/run-work-order-loop-tool.js.map +1 -0
- package/dist/verified-identity.d.ts +54 -0
- package/dist/verified-identity.d.ts.map +1 -0
- package/dist/verified-identity.js +133 -0
- package/dist/verified-identity.js.map +1 -0
- package/dist/work-order-loop.d.ts +82 -0
- package/dist/work-order-loop.d.ts.map +1 -0
- package/dist/work-order-loop.js +149 -0
- package/dist/work-order-loop.js.map +1 -0
- package/package.json +59 -0
- package/skill-narrative.md +53 -0
- 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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"}
|