agent-tempo 1.7.0-beta.3 → 1.7.0-beta.5

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-tempo-dashboard",
3
3
  "private": true,
4
- "version": "1.7.0-beta.3",
4
+ "version": "1.7.0-beta.5",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -38,42 +38,37 @@ export interface RunHeadlessPiOptions {
38
38
  continueSessionId?: string;
39
39
  }
40
40
  /**
41
- * Build the `DefaultResourceLoader` options for a headless Pi player.
41
+ * #715 — compute the registration-level `excludeTools` denylist for
42
+ * `createAgentSession`. Excluded tools are never registered → ABSENT from the
43
+ * model's toolset AND system prompt: the LLM cannot request what it never sees.
42
44
  *
43
- * SECURITY S2 (MD-C deny-list soundness). The `restricted` tool gate is a
44
- * DENY-LIST over shell/exec tool *names* (tool-capability.ts EXEC_TOOLS, via
45
- * `classify(name) === 'exec'` F1 replaced extension.ts's former local set). That
46
- * guarantee "restricted = no host execution" holds ONLY IF no third-party
47
- * extension can register an un-blacklisted execution tool (e.g. a custom
48
- * `python` / `npm` / `run` tool). It therefore depends on a hard structural
49
- * fact: which extensions Pi loads.
45
+ * This is a registration-level FLOOR beneath the call-time MD-C handler + #712
46
+ * gate. It is tamper-RESISTANT, NOT tamper-PROOF: it defends a PROMPT-INJECTED
47
+ * agent (and holds even if the call-time gate had a bug — the tool simply isn't
48
+ * there), but it does NOT defend against PROCESS COMPROMISE a tampered /
49
+ * modified extension can re-register or un-exclude tools (this is OUR code
50
+ * passing a denylist; an attacker who modifies the code/process bypasses it).
51
+ * That residual is OS-sandbox + supply-chain integrity, tracked as #724.
50
52
  *
51
- * Verified against the installed Pi SDK 0.78 source (NOT assumed):
52
- * - `DefaultResourceLoader.reload()` (resource-loader.js:271-276) builds
53
- * `extensionPaths = noExtensions ? cliEnabledExtensions
54
- * : merge(cliEnabledExtensions, enabledExtensions)`
55
- * where `enabledExtensions` (line 229) are the DISK/package extensions from
56
- * `packageManager.resolve()` (`~/.pi/agent/extensions/`, `<cwd>/.pi/extensions/`,
57
- * installed packages). `loadExtensions(extensionPaths)` then loads them and
58
- * MERGES with our inline factories (lines 274-276).
59
- * - `noExtensions` defaults to `false` (constructor, line 132) so the naive
60
- * loader DOES load disk extensions. That is the S2 gap.
53
+ * `excludeTools` is matched by NAME against BOTH Pi built-ins AND
54
+ * extension-registered tools (incl. agent-tempo's MCP tools via `renderToPi`), so
55
+ * this list contains ONLY Pi-built-in / exec names — never agent-tempo tool names
56
+ * (`cue`/`report`/`recruit`/…). Posture:
57
+ * - `toolAccess === 'restricted'` exclude {@link EXEC_TOOLS} (exec/bash
58
+ * registration-absent; a strict upgrade of the prior call-time block, and the
59
+ * headless default the model never even sees exec).
60
+ * - `guardrailPolicy === 'observe-only'` also exclude the Pi built-in act
61
+ * tools ({@link PI_BUILTIN_ACT_TOOLS}); read/grep/glob stay. The agent-tempo
62
+ * MCP act tools (recruit/destroy/…) stay covered by the client-side no-act
63
+ * handler (commit 5) — excludeTools handles the Pi built-ins only.
64
+ * - `monitored` / `supervised` / `autonomous` → NO exec exclusion: those tools
65
+ * stay REGISTERED so they can be gated/approved per-use (#712). (`supervised`
66
+ * = approve-and-run, NOT exec-absent.)
61
67
  *
62
- * Fix (= security's "exclude the extensions dir", done structurally):
63
- * - `noExtensions: true` `extensionPaths` collapses to `cliEnabledExtensions`,
64
- * which is empty because we pass NO `additionalExtensionPaths`. So
65
- * `loadExtensions([])` registers nothing from disk/packages.
66
- * - Inline `extensionFactories` load UNCONDITIONALLY (reload() line 275 is not
67
- * gated by `noExtensions`), so our agent-tempo extension still attaches.
68
- * Net: the ONLY tools present are Pi's built-ins (bash/read/edit/write/grep —
69
- * all covered by the deny-list) + our agent-tempo MCP tools (no exec). No
70
- * third-party tool can slip past the deny-list. Skills/prompts/themes cannot
71
- * register tools, so they are not a vector and are left at defaults.
72
- *
73
- * Kept as a pure, exported helper so the `noExtensions: true` invariant has a
74
- * unit regression test (test/pi-headless-loader.test.ts) without needing the Pi
75
- * SDK installed.
68
+ * Pure + exported so the registration-absence invariant has a unit regression
69
+ * test without the Pi SDK (mirrors {@link buildPiResourceLoaderOptions}).
76
70
  */
71
+ export declare function computeExcludeTools(toolAccess: PiToolAccess, guardrailPolicy: GuardrailPolicy | undefined): string[];
77
72
  export declare function buildPiResourceLoaderOptions(params: {
78
73
  cwd: string;
79
74
  agentDir: string;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeExcludeTools = computeExcludeTools;
3
4
  exports.buildPiResourceLoaderOptions = buildPiResourceLoaderOptions;
4
5
  exports.runHeadlessPi = runHeadlessPi;
5
6
  /**
@@ -31,6 +32,7 @@ exports.runHeadlessPi = runHeadlessPi;
31
32
  const config_1 = require("../config");
32
33
  const sdk_probe_1 = require("../utils/sdk-probe");
33
34
  const extension_1 = require("./extension");
35
+ const tool_capability_1 = require("../security/tool-capability");
34
36
  const probe_1 = require("./probe");
35
37
  const session_seed_1 = require("./session-seed");
36
38
  const log = (...args) => {
@@ -114,6 +116,61 @@ async function resolveModel(modelStr) {
114
116
  * unit regression test (test/pi-headless-loader.test.ts) without needing the Pi
115
117
  * SDK installed.
116
118
  */
119
+ /**
120
+ * Pi BUILT-IN mutating ("act") tool names, excluded at registration for the
121
+ * `observe-only` no-act posture (#715). Pi's default built-ins are
122
+ * read/bash/edit/write; `multiedit` is listed defensively (a no-op if Pi doesn't
123
+ * register it). `bash`/exec are covered by {@link EXEC_TOOLS}. We keep the READ
124
+ * built-ins (read/grep/glob/ls). These are Pi BUILT-IN names — deliberately NOT
125
+ * agent-tempo MCP tool names, so excluding them never removes an agent-tempo
126
+ * coordination tool (those stay handler-gated, commit 5).
127
+ */
128
+ const PI_BUILTIN_ACT_TOOLS = ['write', 'edit', 'multiedit'];
129
+ /**
130
+ * #715 — compute the registration-level `excludeTools` denylist for
131
+ * `createAgentSession`. Excluded tools are never registered → ABSENT from the
132
+ * model's toolset AND system prompt: the LLM cannot request what it never sees.
133
+ *
134
+ * This is a registration-level FLOOR beneath the call-time MD-C handler + #712
135
+ * gate. It is tamper-RESISTANT, NOT tamper-PROOF: it defends a PROMPT-INJECTED
136
+ * agent (and holds even if the call-time gate had a bug — the tool simply isn't
137
+ * there), but it does NOT defend against PROCESS COMPROMISE — a tampered /
138
+ * modified extension can re-register or un-exclude tools (this is OUR code
139
+ * passing a denylist; an attacker who modifies the code/process bypasses it).
140
+ * That residual is OS-sandbox + supply-chain integrity, tracked as #724.
141
+ *
142
+ * `excludeTools` is matched by NAME against BOTH Pi built-ins AND
143
+ * extension-registered tools (incl. agent-tempo's MCP tools via `renderToPi`), so
144
+ * this list contains ONLY Pi-built-in / exec names — never agent-tempo tool names
145
+ * (`cue`/`report`/`recruit`/…). Posture:
146
+ * - `toolAccess === 'restricted'` → exclude {@link EXEC_TOOLS} (exec/bash
147
+ * registration-absent; a strict upgrade of the prior call-time block, and the
148
+ * headless default → the model never even sees exec).
149
+ * - `guardrailPolicy === 'observe-only'` → also exclude the Pi built-in act
150
+ * tools ({@link PI_BUILTIN_ACT_TOOLS}); read/grep/glob stay. The agent-tempo
151
+ * MCP act tools (recruit/destroy/…) stay covered by the client-side no-act
152
+ * handler (commit 5) — excludeTools handles the Pi built-ins only.
153
+ * - `monitored` / `supervised` / `autonomous` → NO exec exclusion: those tools
154
+ * stay REGISTERED so they can be gated/approved per-use (#712). (`supervised`
155
+ * = approve-and-run, NOT exec-absent.)
156
+ *
157
+ * Pure + exported so the registration-absence invariant has a unit regression
158
+ * test without the Pi SDK (mirrors {@link buildPiResourceLoaderOptions}).
159
+ */
160
+ function computeExcludeTools(toolAccess, guardrailPolicy) {
161
+ const exclude = new Set();
162
+ if (toolAccess === 'restricted') {
163
+ for (const t of tool_capability_1.EXEC_TOOLS)
164
+ exclude.add(t);
165
+ }
166
+ if (guardrailPolicy === 'observe-only') {
167
+ for (const t of tool_capability_1.EXEC_TOOLS)
168
+ exclude.add(t); // no-act ⊇ no-exec
169
+ for (const t of PI_BUILTIN_ACT_TOOLS)
170
+ exclude.add(t);
171
+ }
172
+ return [...exclude];
173
+ }
117
174
  function buildPiResourceLoaderOptions(params) {
118
175
  return {
119
176
  cwd: params.cwd,
@@ -192,10 +249,20 @@ async function runHeadlessPi(opts = {}) {
192
249
  // heartbeat). The SDK's own doc comment (sdk.js:74-83) prescribes this exact
193
250
  // construct → reload() → pass-as-resourceLoader sequence.
194
251
  await resourceLoader.reload();
252
+ // #715 — registration-level exec/act exclusion (the true "agent physically
253
+ // lacks the tools" boundary; see computeExcludeTools). Excluded tools are never
254
+ // registered, so they're absent from the model's toolset + system prompt — a
255
+ // hard layer beyond the call-time MD-C handler + #712 gate (kept as
256
+ // belt-and-suspenders). Empty for monitored/supervised/autonomous+standard.
257
+ const excludeTools = computeExcludeTools(toolAccess, opts.guardrailPolicy);
258
+ if (excludeTools.length > 0) {
259
+ log(`#715: excluding ${excludeTools.length} tool(s) at registration (toolAccess=${toolAccess}, guardrailPolicy=${opts.guardrailPolicy ?? 'autonomous'}): ${excludeTools.join(', ')}`);
260
+ }
195
261
  const { session } = await createAgentSession({
196
262
  cwd: process.cwd(),
197
263
  agentDir,
198
264
  ...(model ? { model } : {}),
265
+ ...(excludeTools.length > 0 ? { excludeTools } : {}),
199
266
  resourceLoader,
200
267
  // H1 (#645): in-memory session (seeded above via the session-seed chokepoint).
201
268
  // H2 will seed it from agent-tempo durable state (ENV.PI_CONTINUE_SESSION
package/dist/types.d.ts CHANGED
@@ -238,24 +238,39 @@ export interface SessionMetadata {
238
238
  * the real posture on EVERY attach (across restart / migrate / re-attach), so
239
239
  * a previously-`supervised` agent stays supervised. (tempo-architect ruling.)
240
240
  *
241
- * **★ Enforcement scope (#712 claims EXACTLY this, no more).** The daemon
242
- * enforces fail-closed for a `supervised` player's gate ENGAGEMENTS: the gate's
243
- * `open()` cross-check derives the `failMode` from THIS durable policy (read by
244
- * the daemon, populated at spawn + lazily resolved on a cache-miss), so the
245
- * `failMode` an engaging agent stamps on its `gate_pending` frame can't
246
- * self-downgrade a supervised player out of fail-closed (a `'open'` claim against
247
- * a `supervised` policy is overridden to `closed` and a `failmode-override`
248
- * audit record is written). This is **NOT a hard boundary** against an agent
249
- * that SKIPS gate-engagement entirely (runs the tool without emitting a
250
- * `gate_pending` frame) or bypasses the client-side MD-C exec-block a true
251
- * boundary requires spawn-time tool-restriction (the agent physically lacks the
252
- * tools), tracked separately in **#715**.
241
+ * **★ Enforcement scope (#712/#715).** `supervised` is the daemon-enforced
242
+ * approval boundary for the realistic threat: a prompt-injected agent. A
243
+ * manipulated LLM can only *emit* tool-call requests Pi routes every one to
244
+ * agent-tempo's `tool_call` handler, which engages the gate (non-`low-risk`;
245
+ * #712 daemon-computes `failMode` from this durable policy, falling `closed` on
246
+ * any lookup failure no-fail-open, so an engaging agent can't self-downgrade).
247
+ * The agent **cannot** skip the gate it doesn't control the hook.
248
+ *
249
+ * **#715 adds a registration-level floor.** For `toolAccess: 'restricted'` (and
250
+ * `observe-only`'s act tools) the exec/act tools are EXCLUDED at
251
+ * `createAgentSession` (`excludeTools`) **absent** from the model's toolset and
252
+ * system prompt entirely; the LLM cannot request what it never sees. That is
253
+ * stronger than a call-time block — it holds even if the call-time gate had a bug
254
+ * (the tool simply isn't there). `supervised` with exec present keeps exec
255
+ * **present + gated** (approve-per-use), so this floor applies to the exec/no-act
256
+ * postures, not to a `supervised`+`standard` player.
257
+ *
258
+ * **Residual (all postures): process compromise** — code execution *inside* the
259
+ * Pi process (in-process syscalls; host RCE bypassing the handler), OR a
260
+ * tampered / modified extension that un-excludes or re-registers tools.
261
+ * `excludeTools` is OUR code passing a denylist; an attacker who modifies that
262
+ * code or the process bypasses it. The only defense is OS-level sandboxing +
263
+ * supply-chain integrity, a separate future `'sandboxed'` posture (#724). So:
264
+ * **tamper-RESISTANT** vs prompt-injection + an honest gate bug; **NOT
265
+ * tamper-PROOF** vs a compromised process. Against prompt-injection — the
266
+ * realistic threat — it **is** a real enforcement boundary; #724 is not a gap in
267
+ * that scope.
253
268
  *
254
269
  * **Post-restart window:** on daemon restart the in-memory ingest tokens are
255
270
  * invalidated, so existing players' gate engagements are rejected (403) until a
256
271
  * re-spawn re-mints. In that window a `supervised` player's gate-client
257
- * fail-closes on its own derived deadline (client-side safety holds), but the
258
- * gate is NOT daemon-mediated — the #715 client-cooperative residual.
272
+ * fail-closes on its own derived deadline (client-side safety holds, not
273
+ * daemon-mediated)same process-compromise residual, not a distinct gap.
259
274
  */
260
275
  guardrailPolicy?: GuardrailPolicy;
261
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-tempo",
3
- "version": "1.7.0-beta.3",
3
+ "version": "1.7.0-beta.5",
4
4
  "description": "Many agents, one tempo. Durable coordination for multi-agent work via Temporal.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -72,12 +72,12 @@
72
72
  "copilot-bridge": "ts-node src/adapters/copilot/adapter.ts",
73
73
  "clean:test": "node -e \"require('fs').rmSync('dist-test',{recursive:true,force:true})\"",
74
74
  "build:test": "npm run clean:test && tsc -p test/tsconfig.json",
75
- "pretest": "npm run build:test",
75
+ "pretest": "node scripts/check-bundle-present.js && npm run build:test",
76
76
  "test:tui": "vitest run",
77
77
  "test:conformance": "npm run build:test && mocha --config .mocharc.conformance.yml",
78
- "pretest:shard-1": "npm run build:test && npm run build:scripts",
78
+ "pretest:shard-1": "node scripts/check-bundle-present.js && npm run build:test && npm run build:scripts",
79
79
  "test:shard-1": "node dist/scripts/run-shard.js 1",
80
- "pretest:shard-2": "npm run build:test && npm run build:scripts",
80
+ "pretest:shard-2": "node scripts/check-bundle-present.js && npm run build:test && npm run build:scripts",
81
81
  "test:shard-2": "node dist/scripts/run-shard.js 2",
82
82
  "test": "mocha && vitest run",
83
83
  "lint:surface-drift": "node scripts/check-surface-drift.js",