agent-tempo 1.7.0-beta.4 → 1.7.0-beta.6
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/CLAUDE.md +1 -1
- package/dashboard/package.json +1 -1
- package/dist/cli/command-center-command.d.ts +28 -0
- package/dist/cli/command-center-command.js +114 -0
- package/dist/cli/help-text.js +1 -0
- package/dist/cli.js +19 -1
- package/dist/config.d.ts +47 -0
- package/dist/config.js +46 -0
- package/dist/pi/extension.js +21 -0
- package/dist/pi/headless.d.ts +27 -32
- package/dist/pi/headless.js +67 -0
- package/dist/pi/mission-control/extension.d.ts +7 -0
- package/dist/pi/mission-control/extension.js +15 -0
- package/dist/spawn.d.ts +39 -0
- package/dist/spawn.js +32 -0
- package/dist/types.d.ts +26 -15
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -211,7 +211,7 @@ daemon worker notes, `npx ts-node` dev runner).
|
|
|
211
211
|
- **OpenCode adapter** (`agent: 'opencode'`, #449): Headless multi-provider adapter that drives sessions via [SST OpenCode](https://opencode.ai) as a managed subprocess — supports Anthropic, OpenAI, Bedrock, Vertex, Ollama, and ~70 other providers via OpenCode's `provider/model` selector. Requires OpenCode CLI (`npm install -g opencode-ai`) and the `@opencode-ai/sdk` optional dependency. Recruit with `model: 'provider/name'` (e.g. `'anthropic/claude-opus-4-7'`). Tool bridging is MCP-native — OpenCode spawns `dist/server.js` as its own stdio MCP child. Session state is persisted server-side by OpenCode; the adapter stashes the session id on workflow metadata for reconnect across `opencode serve` restarts. See `src/adapters/opencode/`.
|
|
212
212
|
- **Claude Code headless adapter** (`agent: 'claude-code-headless'`, #520): Headless adapter that drives sessions via the official `claude` CLI as a per-turn `claude -p --output-format stream-json` subprocess. The whole point: turns bill against the host's existing Claude Code subscription extra-usage credits (Pro / Max plans) rather than a Console workspace API key — the only ToS-clean way for a third-party tool to tap that pool. Requires the `claude` binary on PATH AND a logged-in Claude Code session (`claude auth login`); recruit pre-flight rejects with an actionable error otherwise. Tool surface is the union of full Claude Code built-ins (Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch) and the agent-tempo MCP surface — registered via inline `--mcp-config` so `claude` spawns `dist/server.js` as its own MCP child (no in-process bridge). Recruit knobs: `permissionMode` (default `'acceptEdits'`) or `dangerouslySkipPermissions: true` (mutually exclusive). Sessions resume across restart via the existing `sessionId` metadata field — the same UUID is shared with the interactive `claude-code` adapter (per-cwd JSONL is per-cwd, not per-adapter). See `src/adapters/claude-code-headless/` and `examples/ensembles/tempo-headless-jam.yaml`.
|
|
213
213
|
- **Pi adapter** (`agent: 'pi'`, #632 / #666): Two modes. **(1) Interactive conductor** (#666): `agent-tempo up --agent pi --ensemble <name>` launches `pi` in a real terminal with the agent-tempo extension auto-loaded (`pi -e dist/pi/extension.js`); the Pi session self-bootstraps its Temporal workflow and attaches as a conductor/player — no separate recruiter step. From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor — set `conductor.agent: pi` in that ensemble's lineup to make it a Pi conductor. Requires `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model). The `AGENT_TEMPO_*` env is auto-wired by `up`; power users can invoke the extension directly with `pi -e dist/pi/extension.js`. `--model provider/model` selector (e.g. `'github-copilot/gpt-4o'`) is a fast-follow. **(2) Headless player** (Phase 3a): `recruit` with `agent: 'pi'` — no terminal, no BaseAttachment; runs `createAgentSession` with an in-memory `SessionManager`; the module-scope singleton owns claim/heartbeat/tools/cue pump (MD-D). MD-C tool-access policy: `toolAccess: 'restricted'` (default — Bash/shell/exec HARD-BLOCKED) | `'standard'` (scoped Bash) | `'full'` (unsandboxed; requires `force: true`). `noExtensions: true` closes the S2 exec-tool-bypass gap. See `src/adapters/pi/` and `src/pi/headless.ts`.
|
|
214
|
-
- **Mission-control
|
|
214
|
+
- **Mission-control / Command-center** (3f, #700 P2): An interactive Pi TUI that is both a live ensemble board + operator controller and an LLM planner. Board side: HTTP-drives the daemon — coarse ensemble view via `/v1/events/:ensemble` SSE + fine per-player tail via `/inner` (T3); operator controls (cue/pause/play/restart/destroy + gate arm/disarm/decide) POST to the daemon write surface using `AGENT_TEMPO_HTTP_ADMIN_TOKEN`. Planner side: registers LLM tools (`ask` / `handoff` / `cue` / `recruit` / `observe_board`) via `MissionControlActions` — HTTP-backed, distinct from the player extension's `renderToPi` MCP surface. **Never claims attachment or registers as a player** — invisible to the ensemble. Launch with **`agent-tempo command-center [ensemble]`** (aliases: `cc`, `board`) — sets `AGENT_TEMPO_MISSION_CONTROL=1`. **Role-gated and mutually exclusive** with the player extension (`resolvePiRole` discriminator: explicit `AGENT_TEMPO_PI_ROLE` → `PLAYER_NAME` present → `AGENT_TEMPO_MISSION_CONTROL` → `none`): a bare `pi` keeps both extensions dormant (plain coding session); a player session (`up --agent pi` / `recruit`) keeps the board dormant. Power users invoking `pi -e dist/pi/extension.js` directly without `PLAYER_NAME` also resolve to `none` (dormant) — set `AGENT_TEMPO_PI_ROLE=player` to force player mode. See `src/pi/mission-control/`.
|
|
215
215
|
- **Command-center planner** (#700 P2): An inbox-less interactive Pi session (the operator's planning seat) that routes questions to players via correlated `cue` tags (`[Q <questionId>]`). Players answer with the `respond` MCP tool, which parks the answer on the per-ensemble maestro Q&A mailbox (TTL 1h, 20-slot cap). The planner is woken by an `answer` SSE event when the answer lands (`docs/SSE-PROTOCOL.md` §6). `/handoff` cues hand active work to a conductor (a registered player with a Temporal inbox). See `docs/concepts.md` for the Q&A mechanics.
|
|
216
216
|
- **Guardrail policy** (`guardrailPolicy` recruit arg, #700 P2): Per-player posture for the operator gate on headless Pi players. Four values — `'autonomous'` (default; no gate), `'monitored'` (gate engaged; fail-open: auto-allow after 45 s), `'supervised'` (gate engaged; fail-closed: auto-deny after 300 s; **client-cooperative in P2, not tamper-proof**; daemon-side enforcement is P2.1 #44), `'observe-only'` (non-`low-risk` tools hard-blocked outright). Persisted on `SessionMetadata`; re-sourced at each `(re)attach` from `getMetadata`. See `docs/concepts.md` for operator gate mechanics.
|
|
217
217
|
- **Mock adapter** (`agent: 'mock'`, dev mode only): Four modes: `echo` (echoes input), `scripted` (replays YAML scenario rules), `silent` (drains messages without replying — heartbeat-stale validation), `chaos` (probabilistic fail/crash injection via seeded PRNG). Only registered when `isDevMode()` is true; stripped from the npm tarball by `prepack`. See `src/adapters/mock/`.
|
package/dashboard/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-tempo-dashboard",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "1.7.0-beta.
|
|
4
|
+
"version": "1.7.0-beta.6",
|
|
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": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-tempo command-center` (#729) — launch an interactive Pi mission-control
|
|
3
|
+
* board for an ensemble.
|
|
4
|
+
*
|
|
5
|
+
* The board is the OPERATOR seat: it observes the ensemble (coarse SSE) and POSTs
|
|
6
|
+
* operator actions (cue/pause/restart/gate/…) to the daemon's write/gate surface.
|
|
7
|
+
* It is NOT a player — it never claims attachment or registers as an ensemble
|
|
8
|
+
* member.
|
|
9
|
+
*
|
|
10
|
+
* The deliberate launch path (#729 A2): this sets `AGENT_TEMPO_MISSION_CONTROL=1`
|
|
11
|
+
* so {@link resolvePiRole} resolves `'command-center'`, which activates the
|
|
12
|
+
* mission-control extension and keeps the player extension dormant — exactly one
|
|
13
|
+
* of the two auto-loaded Pi extensions registers tools, so their `cue`/`recruit`
|
|
14
|
+
* names can't collide. It MUST NOT set `PLAYER_NAME`/`CONDUCTOR` (those flip the
|
|
15
|
+
* role to player). A bare `pi` (neither signal) keeps BOTH dormant — plain coding
|
|
16
|
+
* sessions stay pristine.
|
|
17
|
+
*/
|
|
18
|
+
import { CliOverrides } from '../config';
|
|
19
|
+
export interface CommandCenterArgs extends CliOverrides {
|
|
20
|
+
/** Ensemble to observe (positional / `--ensemble`); falls back to config default. */
|
|
21
|
+
ensemble?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Public entry point — invoked by the CLI dispatcher. Launches the board in a
|
|
25
|
+
* real terminal (Pi only attaches its UI in a TTY) and returns; exits 1 on a
|
|
26
|
+
* fail-clean preflight error.
|
|
27
|
+
*/
|
|
28
|
+
export declare function commandCenterCommand(args: CommandCenterArgs): Promise<void>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.commandCenterCommand = commandCenterCommand;
|
|
37
|
+
/**
|
|
38
|
+
* `agent-tempo command-center` (#729) — launch an interactive Pi mission-control
|
|
39
|
+
* board for an ensemble.
|
|
40
|
+
*
|
|
41
|
+
* The board is the OPERATOR seat: it observes the ensemble (coarse SSE) and POSTs
|
|
42
|
+
* operator actions (cue/pause/restart/gate/…) to the daemon's write/gate surface.
|
|
43
|
+
* It is NOT a player — it never claims attachment or registers as an ensemble
|
|
44
|
+
* member.
|
|
45
|
+
*
|
|
46
|
+
* The deliberate launch path (#729 A2): this sets `AGENT_TEMPO_MISSION_CONTROL=1`
|
|
47
|
+
* so {@link resolvePiRole} resolves `'command-center'`, which activates the
|
|
48
|
+
* mission-control extension and keeps the player extension dormant — exactly one
|
|
49
|
+
* of the two auto-loaded Pi extensions registers tools, so their `cue`/`recruit`
|
|
50
|
+
* names can't collide. It MUST NOT set `PLAYER_NAME`/`CONDUCTOR` (those flip the
|
|
51
|
+
* role to player). A bare `pi` (neither signal) keeps BOTH dormant — plain coding
|
|
52
|
+
* sessions stay pristine.
|
|
53
|
+
*/
|
|
54
|
+
const config_1 = require("../config");
|
|
55
|
+
const spawn_1 = require("../spawn");
|
|
56
|
+
const probe_1 = require("../pi/probe");
|
|
57
|
+
const out = __importStar(require("./output"));
|
|
58
|
+
/**
|
|
59
|
+
* Public entry point — invoked by the CLI dispatcher. Launches the board in a
|
|
60
|
+
* real terminal (Pi only attaches its UI in a TTY) and returns; exits 1 on a
|
|
61
|
+
* fail-clean preflight error.
|
|
62
|
+
*/
|
|
63
|
+
async function commandCenterCommand(args) {
|
|
64
|
+
const config = (0, config_1.getConfig)(args);
|
|
65
|
+
const ensemble = config.ensemble;
|
|
66
|
+
// Preflight — fail BEFORE launching a terminal that would die. checkPiNodeFloor
|
|
67
|
+
// is a best-effort Node-version proxy; buildPiCommandCenterSpawn's default
|
|
68
|
+
// resolver throws fail-clean when the Pi CLI is missing.
|
|
69
|
+
const nodeFloor = (0, probe_1.checkPiNodeFloor)();
|
|
70
|
+
if (!nodeFloor.ok) {
|
|
71
|
+
out.error(`Cannot start command-center — ${nodeFloor.reason}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
// Admin (T3) token — mission-control's operator write/gate surface reads it.
|
|
75
|
+
// Without it the board still OBSERVES (coarse SSE), but operator actions return
|
|
76
|
+
// 401; warn rather than block so a read-only board is still useful.
|
|
77
|
+
const adminToken = process.env[config_1.ENV.HTTP_ADMIN_TOKEN];
|
|
78
|
+
if (!adminToken) {
|
|
79
|
+
out.warn(`${config_1.ENV.HTTP_ADMIN_TOKEN} is not set — the board will observe read-only; operator ` +
|
|
80
|
+
'actions (cue/pause/restart/gate) need the admin token. Export it before launching for full control.');
|
|
81
|
+
}
|
|
82
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
83
|
+
out.warn('ANTHROPIC_API_KEY is not set — the Pi command-center will fall back to Pi\'s own auth/default model.');
|
|
84
|
+
}
|
|
85
|
+
const temporalEnvVars = {
|
|
86
|
+
[config_1.ENV.TEMPORAL_ADDRESS]: config.temporalAddress,
|
|
87
|
+
[config_1.ENV.TEMPORAL_NAMESPACE]: config.temporalNamespace,
|
|
88
|
+
};
|
|
89
|
+
if (config.temporalApiKey)
|
|
90
|
+
temporalEnvVars[config_1.ENV.TEMPORAL_API_KEY] = config.temporalApiKey;
|
|
91
|
+
if (config.temporalTlsCertPath)
|
|
92
|
+
temporalEnvVars[config_1.ENV.TEMPORAL_TLS_CERT_PATH] = config.temporalTlsCertPath;
|
|
93
|
+
if (config.temporalTlsKeyPath)
|
|
94
|
+
temporalEnvVars[config_1.ENV.TEMPORAL_TLS_KEY_PATH] = config.temporalTlsKeyPath;
|
|
95
|
+
let spawn;
|
|
96
|
+
try {
|
|
97
|
+
spawn = (0, spawn_1.buildPiCommandCenterSpawn)({
|
|
98
|
+
ensemble,
|
|
99
|
+
temporalEnvVars,
|
|
100
|
+
taskQueue: config.taskQueue,
|
|
101
|
+
devMode: (0, config_1.isDevMode)(),
|
|
102
|
+
...(adminToken ? { adminToken } : {}),
|
|
103
|
+
...(process.env.ANTHROPIC_API_KEY ? { anthropicApiKey: process.env.ANTHROPIC_API_KEY } : {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
out.error(`Cannot start command-center — ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const { pid } = (0, spawn_1.launchInTerminal)(spawn.cmd, spawn.args, process.cwd(), spawn.env);
|
|
111
|
+
out.success(`Command-center launched for ensemble ${out.cyan(ensemble)} (pid ${pid ?? 'unknown'})`);
|
|
112
|
+
out.log(` ${out.dim('Observer-only Pi board — the player extension stays dormant in this session.')}`);
|
|
113
|
+
out.log(` ${out.dim('Requires `agent-tempo install-pi` to have registered the extensions.')}`);
|
|
114
|
+
}
|
package/dist/cli/help-text.js
CHANGED
|
@@ -86,6 +86,7 @@ ${out.bold('Commands:')}
|
|
|
86
86
|
${out.cyan('agent-types')} <sub> Manage player type definitions (list/show/init)
|
|
87
87
|
${out.cyan('daemon')} <sub> Manage the worker daemon (start/stop/status/logs)
|
|
88
88
|
${out.cyan('dashboard')} Open the web dashboard (--no-open / --pair / --json)
|
|
89
|
+
${out.cyan('command-center')} [ensemble] Launch the interactive Pi mission-control board (operator seat; alias: cc/board)
|
|
89
90
|
${out.cyan('upgrade')} [version] Upgrade agent-tempo to latest (or specific version)
|
|
90
91
|
${out.cyan('config')} Configure Temporal connection settings
|
|
91
92
|
${out.cyan('init')} Register MCP server globally (or --project for .mcp.json)
|
package/dist/cli.js
CHANGED
|
@@ -416,6 +416,18 @@ async function main() {
|
|
|
416
416
|
});
|
|
417
417
|
return;
|
|
418
418
|
}
|
|
419
|
+
if (args.command === 'command-center' || args.command === 'cc' || args.command === 'board') {
|
|
420
|
+
// #729 — launch the interactive Pi mission-control board. Lightweight + does
|
|
421
|
+
// NOT touch Temporal (it spawns `pi` with the operator opt-in env and drives
|
|
422
|
+
// the daemon over HTTP), so it stays out of the Temporal-loading `./cli/commands`
|
|
423
|
+
// import — an operator can open the board even when the Temporal SDK is broken.
|
|
424
|
+
const { commandCenterCommand } = await Promise.resolve().then(() => __importStar(require('./cli/command-center-command')));
|
|
425
|
+
await commandCenterCommand({
|
|
426
|
+
ensemble: args.positional[1],
|
|
427
|
+
...overrides,
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
419
431
|
// Dev-mode scriptable verbs (#432). Gated on `isDevMode()` AND an explicit
|
|
420
432
|
// allowlist (`DEV_VERBS`); intercepts BEFORE the removed-verbs check so
|
|
421
433
|
// verbs like `pause` (collapsed by #288) can act on the live ensemble in
|
|
@@ -631,7 +643,13 @@ async function main() {
|
|
|
631
643
|
for (const p of result.alreadyPresent)
|
|
632
644
|
out.log(out.dim(` · ${p} (already installed)`));
|
|
633
645
|
out.log('');
|
|
634
|
-
|
|
646
|
+
// #729 — the extensions are role-gated: exactly one activates per session.
|
|
647
|
+
// Point operators at the DELIBERATE launches, not a bare `pi` (which now
|
|
648
|
+
// intentionally keeps BOTH dormant so plain coding sessions stay pristine).
|
|
649
|
+
out.log(' Launch a session with one of:');
|
|
650
|
+
out.log(` ${out.cyan('agent-tempo up --agent pi')} a conductor/player in this ensemble`);
|
|
651
|
+
out.log(` ${out.cyan('agent-tempo command-center')} the operator mission-control board`);
|
|
652
|
+
out.log(out.dim(' A bare `pi` stays a plain coding session (neither extension activates).'));
|
|
635
653
|
break;
|
|
636
654
|
}
|
|
637
655
|
case 'migrate-from-claude-tempo': {
|
package/dist/config.d.ts
CHANGED
|
@@ -158,7 +158,54 @@ export declare const ENV: {
|
|
|
158
158
|
* coordinate three or more parallel agent-tempo profiles on one box.
|
|
159
159
|
*/
|
|
160
160
|
readonly DEV_HOME_OVERRIDE: "AGENT_TEMPO_HOME_OVERRIDE";
|
|
161
|
+
/**
|
|
162
|
+
* #729 — explicit Pi session-role override for {@link resolvePiRole}. Accepts
|
|
163
|
+
* `'player'` | `'command-center'`. A future escape hatch: the heuristic
|
|
164
|
+
* (PLAYER_NAME presence) already classifies every current launch correctly, so
|
|
165
|
+
* this is intentionally NOT wired into any spawn site — it exists so an operator
|
|
166
|
+
* can force a role if a new launch path ever defeats the heuristic.
|
|
167
|
+
*/
|
|
168
|
+
readonly PI_ROLE: "AGENT_TEMPO_PI_ROLE";
|
|
169
|
+
/**
|
|
170
|
+
* #729 (A2) — affirmative opt-in for the mission-control command-center board.
|
|
171
|
+
* Set by the `agent-tempo command-center` launcher (NOT by bare `pi`), so a
|
|
172
|
+
* plain coding `pi` stays pristine (both extensions dormant). Lower precedence
|
|
173
|
+
* than {@link PLAYER_NAME} in {@link resolvePiRole} — a session that must CLAIM
|
|
174
|
+
* never silently degrades to a passive board.
|
|
175
|
+
*/
|
|
176
|
+
readonly MISSION_CONTROL: "AGENT_TEMPO_MISSION_CONTROL";
|
|
161
177
|
};
|
|
178
|
+
/**
|
|
179
|
+
* The role of a Pi session (#729). A session is at most ONE of these — the two
|
|
180
|
+
* auto-loaded Pi extensions gate on it so they're mutually exclusive:
|
|
181
|
+
* - `player` — the player extension's full MCP surface (cue/recruit/report/…).
|
|
182
|
+
* - `command-center` — mission-control's operator board + planner tools.
|
|
183
|
+
* - `none` — neither (a bare `pi` used for plain coding); both stay dormant.
|
|
184
|
+
*
|
|
185
|
+
* `player` and `command-center` both register `cue`/`recruit` by name, so loading
|
|
186
|
+
* both in one session collides and the command-center never starts (#729).
|
|
187
|
+
*/
|
|
188
|
+
export type PiRole = 'player' | 'command-center' | 'none';
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the role of a Pi session (#729) — the single discriminator both
|
|
191
|
+
* auto-loaded Pi extensions gate on.
|
|
192
|
+
*
|
|
193
|
+
* Precedence (deterministic, #729 A2):
|
|
194
|
+
* 1. Explicit {@link ENV.PI_ROLE} (`'player'` | `'command-center'`) wins — a future
|
|
195
|
+
* escape hatch, intentionally NOT wired into any spawn site.
|
|
196
|
+
* 2. {@link ENV.PLAYER_NAME} present (every `up`/`recruit` player spawn) → `'player'`.
|
|
197
|
+
* Checked BEFORE the board opt-in ON PURPOSE: a session that must CLAIM must
|
|
198
|
+
* never silently degrade to a passive board.
|
|
199
|
+
* 3. {@link ENV.MISSION_CONTROL} opt-in (set by `agent-tempo command-center`) →
|
|
200
|
+
* `'command-center'`.
|
|
201
|
+
* 4. Otherwise `'none'` — a bare `pi` for plain coding: BOTH extensions stay
|
|
202
|
+
* dormant (the A2 clean-pi guarantee).
|
|
203
|
+
*
|
|
204
|
+
* Pure (env injectable) so the dormancy matrix is unit-testable without spawning.
|
|
205
|
+
* NOTE: headless Pi is definitionally a recruited player and must NOT rely on this
|
|
206
|
+
* heuristic — its caller forces `'player'` directly (env-weirdness immune).
|
|
207
|
+
*/
|
|
208
|
+
export declare function resolvePiRole(env?: NodeJS.ProcessEnv): PiRole;
|
|
162
209
|
export interface Config {
|
|
163
210
|
temporalAddress: string;
|
|
164
211
|
temporalNamespace: string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GLOBAL_MAESTRO_WORKFLOW_ID = exports.DaemonConfigSchema = exports.CleanupPolicySchema = exports.CONFIG_FILE_PATH = exports.AGENT_TEMPO_HOME = exports.PROD_DAEMON_PORT = exports.DEV_DAEMON_PORT = exports.PROD_TASK_QUEUE = exports.DEV_TASK_QUEUE = exports.PROD_TEMPORAL_NAMESPACE = exports.DEV_TEMPORAL_NAMESPACE = exports.PROD_HOME_DIR_NAME = exports.DEV_HOME_DIR_NAME = exports.ENV = void 0;
|
|
4
|
+
exports.resolvePiRole = resolvePiRole;
|
|
4
5
|
exports.isDevMode = isDevMode;
|
|
5
6
|
exports.resolveTempoHome = resolveTempoHome;
|
|
6
7
|
exports.bridgeLogsRoot = bridgeLogsRoot;
|
|
@@ -186,7 +187,52 @@ exports.ENV = {
|
|
|
186
187
|
* coordinate three or more parallel agent-tempo profiles on one box.
|
|
187
188
|
*/
|
|
188
189
|
DEV_HOME_OVERRIDE: 'AGENT_TEMPO_HOME_OVERRIDE',
|
|
190
|
+
/**
|
|
191
|
+
* #729 — explicit Pi session-role override for {@link resolvePiRole}. Accepts
|
|
192
|
+
* `'player'` | `'command-center'`. A future escape hatch: the heuristic
|
|
193
|
+
* (PLAYER_NAME presence) already classifies every current launch correctly, so
|
|
194
|
+
* this is intentionally NOT wired into any spawn site — it exists so an operator
|
|
195
|
+
* can force a role if a new launch path ever defeats the heuristic.
|
|
196
|
+
*/
|
|
197
|
+
PI_ROLE: 'AGENT_TEMPO_PI_ROLE',
|
|
198
|
+
/**
|
|
199
|
+
* #729 (A2) — affirmative opt-in for the mission-control command-center board.
|
|
200
|
+
* Set by the `agent-tempo command-center` launcher (NOT by bare `pi`), so a
|
|
201
|
+
* plain coding `pi` stays pristine (both extensions dormant). Lower precedence
|
|
202
|
+
* than {@link PLAYER_NAME} in {@link resolvePiRole} — a session that must CLAIM
|
|
203
|
+
* never silently degrades to a passive board.
|
|
204
|
+
*/
|
|
205
|
+
MISSION_CONTROL: 'AGENT_TEMPO_MISSION_CONTROL',
|
|
189
206
|
};
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the role of a Pi session (#729) — the single discriminator both
|
|
209
|
+
* auto-loaded Pi extensions gate on.
|
|
210
|
+
*
|
|
211
|
+
* Precedence (deterministic, #729 A2):
|
|
212
|
+
* 1. Explicit {@link ENV.PI_ROLE} (`'player'` | `'command-center'`) wins — a future
|
|
213
|
+
* escape hatch, intentionally NOT wired into any spawn site.
|
|
214
|
+
* 2. {@link ENV.PLAYER_NAME} present (every `up`/`recruit` player spawn) → `'player'`.
|
|
215
|
+
* Checked BEFORE the board opt-in ON PURPOSE: a session that must CLAIM must
|
|
216
|
+
* never silently degrade to a passive board.
|
|
217
|
+
* 3. {@link ENV.MISSION_CONTROL} opt-in (set by `agent-tempo command-center`) →
|
|
218
|
+
* `'command-center'`.
|
|
219
|
+
* 4. Otherwise `'none'` — a bare `pi` for plain coding: BOTH extensions stay
|
|
220
|
+
* dormant (the A2 clean-pi guarantee).
|
|
221
|
+
*
|
|
222
|
+
* Pure (env injectable) so the dormancy matrix is unit-testable without spawning.
|
|
223
|
+
* NOTE: headless Pi is definitionally a recruited player and must NOT rely on this
|
|
224
|
+
* heuristic — its caller forces `'player'` directly (env-weirdness immune).
|
|
225
|
+
*/
|
|
226
|
+
function resolvePiRole(env = process.env) {
|
|
227
|
+
const explicit = env[exports.ENV.PI_ROLE];
|
|
228
|
+
if (explicit === 'player' || explicit === 'command-center')
|
|
229
|
+
return explicit;
|
|
230
|
+
if (env[exports.ENV.PLAYER_NAME])
|
|
231
|
+
return 'player';
|
|
232
|
+
if (env[exports.ENV.MISSION_CONTROL])
|
|
233
|
+
return 'command-center';
|
|
234
|
+
return 'none';
|
|
235
|
+
}
|
|
190
236
|
// ── Dev profile (ADR 0014 §5) ──
|
|
191
237
|
/**
|
|
192
238
|
* Dev profile defaults — one switch (`--dev` top-level flag, or
|
package/dist/pi/extension.js
CHANGED
|
@@ -158,6 +158,27 @@ function createPiExtension(options = {}) {
|
|
|
158
158
|
// #700 (P2 / G) — guardrail posture; absent ⇒ autonomous (no gate).
|
|
159
159
|
const guardrailPolicy = options.guardrailPolicy ?? 'autonomous';
|
|
160
160
|
return function piExtension(pi) {
|
|
161
|
+
// ── #729 role gate (MUST stay first — before probePi / getConfig /
|
|
162
|
+
// clientFactory / renderToPi / before_agent_start / any session_start claim) ──
|
|
163
|
+
//
|
|
164
|
+
// Headless Pi is definitionally a recruited player, so force 'player' on it —
|
|
165
|
+
// never trust the env heuristic there (env-weirdness immune). Every INTERACTIVE
|
|
166
|
+
// player spawn (conductor `up --agent pi`, `recruit`) sets PLAYER_NAME; a bare
|
|
167
|
+
// `pi` does not. When this session is NOT a player, the player extension goes
|
|
168
|
+
// FULLY DORMANT: it registers NO tools, opens NO Temporal connection (the
|
|
169
|
+
// `clientFactory(config)` kickoff below is skipped), and starts NO workflow.
|
|
170
|
+
//
|
|
171
|
+
// This is load-bearing for two things at once: (1) it resolves the cue/recruit
|
|
172
|
+
// tool-name COLLISION with mission-control (exactly one extension registers
|
|
173
|
+
// tools per session), and (2) it kills the phantom `pi-${process.pid}` orphan
|
|
174
|
+
// workflow a bare `pi` used to self-bootstrap (the `pi-${pid}` fallback id below
|
|
175
|
+
// is now never reached in a non-player session).
|
|
176
|
+
const role = mode === 'headless' ? 'player' : (0, config_1.resolvePiRole)();
|
|
177
|
+
if (role !== 'player') {
|
|
178
|
+
log(`dormant — session role is '${role}', not a player: registering no player ` +
|
|
179
|
+
'surface (no tools, no Temporal connection, no workflow).');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
161
182
|
const probe = (0, probe_1.probePi)();
|
|
162
183
|
if (!probe.available)
|
|
163
184
|
log('WARNING:', probe.reason);
|
package/dist/pi/headless.d.ts
CHANGED
|
@@ -38,42 +38,37 @@ export interface RunHeadlessPiOptions {
|
|
|
38
38
|
continueSessionId?: string;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* extension can register
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
*
|
|
63
|
-
*
|
|
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;
|
package/dist/pi/headless.js
CHANGED
|
@@ -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
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PiRole } from '../../config';
|
|
1
2
|
import { type BoardModel } from './board';
|
|
2
3
|
import { MissionControlActions, type ActionResult } from './actions';
|
|
3
4
|
import { type InfraProgress } from '../../cli/ensure-infra';
|
|
@@ -25,6 +26,12 @@ export interface MissionControlDeps {
|
|
|
25
26
|
renderThrottleMs?: number;
|
|
26
27
|
/** Local daemon host for tailability (test override; defaults to `os.hostname()`). */
|
|
27
28
|
localHost?: string;
|
|
29
|
+
/**
|
|
30
|
+
* #729 — session-role override for the dormancy gate (test seam). Defaults to
|
|
31
|
+
* {@link resolvePiRole}. The board activates ONLY for `'command-center'`;
|
|
32
|
+
* `'player'` and `'none'` both keep it dormant.
|
|
33
|
+
*/
|
|
34
|
+
role?: PiRole;
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
30
37
|
* Infra-bootstrap seam (#700 P1). Defaults to the real {@link ensureInfra}; the
|
|
@@ -517,6 +517,21 @@ const log = (...args) => {
|
|
|
517
517
|
*/
|
|
518
518
|
function createMissionControlExtension(deps = {}) {
|
|
519
519
|
return (pi) => {
|
|
520
|
+
// ── #729 role gate (MUST stay first — before any registerCommand /
|
|
521
|
+
// registerTool / session_start SSE) ──
|
|
522
|
+
//
|
|
523
|
+
// The board activates ONLY in the command-center role (the operator's
|
|
524
|
+
// `agent-tempo command-center` seat, which sets AGENT_TEMPO_MISSION_CONTROL).
|
|
525
|
+
// A player session ('player') AND a bare coding `pi` ('none') both keep the
|
|
526
|
+
// board DORMANT — no commands/tools registered, no coarse SSE opened. This is
|
|
527
|
+
// the other half of #729 mutual exclusion: exactly one extension registers
|
|
528
|
+
// tools per session (no cue/recruit name collision with the player surface),
|
|
529
|
+
// and a plain `pi` stays pristine (A2 — affirmative opt-in only).
|
|
530
|
+
const role = deps.role ?? (0, config_1.resolvePiRole)();
|
|
531
|
+
if (role !== 'command-center') {
|
|
532
|
+
log(`dormant — session role is '${role}', not command-center: board not activated.`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
520
535
|
const ensemble = deps.ensemble ?? (0, config_1.getConfig)().ensemble;
|
|
521
536
|
const adminToken = deps.adminToken ?? process.env[actions_1.ADMIN_TOKEN_ENV];
|
|
522
537
|
const throttleMs = deps.renderThrottleMs ?? DEFAULT_RENDER_THROTTLE_MS;
|
package/dist/spawn.d.ts
CHANGED
|
@@ -174,6 +174,45 @@ export declare function buildPiConductorSpawn(opts: PiConductorSpawnOpts): {
|
|
|
174
174
|
args: string[];
|
|
175
175
|
env: Record<string, string>;
|
|
176
176
|
};
|
|
177
|
+
/** Inputs for {@link buildPiCommandCenterSpawn} (pure — unit-tested without spawning). */
|
|
178
|
+
export interface PiCommandCenterSpawnOpts {
|
|
179
|
+
ensemble: string;
|
|
180
|
+
/** Temporal env (address/namespace/api-key/tls) built by the caller. */
|
|
181
|
+
temporalEnvVars: Record<string, string>;
|
|
182
|
+
/** Temporal task queue (forwarded for config parity; the board drives the daemon via HTTP). */
|
|
183
|
+
taskQueue: string;
|
|
184
|
+
devMode: boolean;
|
|
185
|
+
/** Daemon admin (T3) token → `AGENT_TEMPO_HTTP_ADMIN_TOKEN` (mission-control's write/gate surface). */
|
|
186
|
+
adminToken?: string;
|
|
187
|
+
/** Forwarded if set (Pi's own model auth). */
|
|
188
|
+
anthropicApiKey?: string;
|
|
189
|
+
/** Injectable resolver (defaults to the real one, which fails clean on miss). */
|
|
190
|
+
resolveBinary?: () => {
|
|
191
|
+
cmd: string;
|
|
192
|
+
args: string[];
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build the interactive Pi COMMAND-CENTER (mission-control) spawn spec —
|
|
197
|
+
* `{ cmd, args, env }` for {@link launchInTerminal} (#729). PURE + injectable.
|
|
198
|
+
*
|
|
199
|
+
* Unlike {@link buildPiConductorSpawn}, this passes NO `-e <ext>`: install-pi
|
|
200
|
+
* registers BOTH Pi extensions in `~/.pi/agent/settings.json`, so a plain `pi`
|
|
201
|
+
* auto-loads them and {@link resolvePiRole} (via the env below) picks exactly one.
|
|
202
|
+
* Passing `-e` here would DOUBLE-LOAD mission-control (settings.json + `-e`) → a
|
|
203
|
+
* command re-registration error. The env carries the OPERATOR subset only:
|
|
204
|
+
* - `AGENT_TEMPO_MISSION_CONTROL=1` → the role opt-in (resolver → `command-center`).
|
|
205
|
+
* - `AGENT_TEMPO_ENSEMBLE` → which ensemble the board observes.
|
|
206
|
+
* - `AGENT_TEMPO_HTTP_ADMIN_TOKEN` → the daemon write/gate surface the board POSTs to.
|
|
207
|
+
*
|
|
208
|
+
* ★ It MUST NOT set `PLAYER_NAME` or `CONDUCTOR` — either flips {@link resolvePiRole}
|
|
209
|
+
* to `'player'`, which would keep the board dormant (and self-bootstrap a player).
|
|
210
|
+
*/
|
|
211
|
+
export declare function buildPiCommandCenterSpawn(opts: PiCommandCenterSpawnOpts): {
|
|
212
|
+
cmd: string;
|
|
213
|
+
args: string[];
|
|
214
|
+
env: Record<string, string>;
|
|
215
|
+
};
|
|
177
216
|
export interface CopilotBridgeOpts {
|
|
178
217
|
name: string;
|
|
179
218
|
ensemble: string;
|
package/dist/spawn.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.spawnInTerminal = spawnInTerminal;
|
|
|
16
16
|
exports.resolvePiInteractiveBinary = resolvePiInteractiveBinary;
|
|
17
17
|
exports.resolvePiExtensionPath = resolvePiExtensionPath;
|
|
18
18
|
exports.buildPiConductorSpawn = buildPiConductorSpawn;
|
|
19
|
+
exports.buildPiCommandCenterSpawn = buildPiCommandCenterSpawn;
|
|
19
20
|
exports.spawnCopilotBridge = spawnCopilotBridge;
|
|
20
21
|
exports.spawnMockAdapter = spawnMockAdapter;
|
|
21
22
|
exports.spawnClaudeApiAdapter = spawnClaudeApiAdapter;
|
|
@@ -673,6 +674,37 @@ function buildPiConductorSpawn(opts) {
|
|
|
673
674
|
};
|
|
674
675
|
return { cmd, args, env };
|
|
675
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Build the interactive Pi COMMAND-CENTER (mission-control) spawn spec —
|
|
679
|
+
* `{ cmd, args, env }` for {@link launchInTerminal} (#729). PURE + injectable.
|
|
680
|
+
*
|
|
681
|
+
* Unlike {@link buildPiConductorSpawn}, this passes NO `-e <ext>`: install-pi
|
|
682
|
+
* registers BOTH Pi extensions in `~/.pi/agent/settings.json`, so a plain `pi`
|
|
683
|
+
* auto-loads them and {@link resolvePiRole} (via the env below) picks exactly one.
|
|
684
|
+
* Passing `-e` here would DOUBLE-LOAD mission-control (settings.json + `-e`) → a
|
|
685
|
+
* command re-registration error. The env carries the OPERATOR subset only:
|
|
686
|
+
* - `AGENT_TEMPO_MISSION_CONTROL=1` → the role opt-in (resolver → `command-center`).
|
|
687
|
+
* - `AGENT_TEMPO_ENSEMBLE` → which ensemble the board observes.
|
|
688
|
+
* - `AGENT_TEMPO_HTTP_ADMIN_TOKEN` → the daemon write/gate surface the board POSTs to.
|
|
689
|
+
*
|
|
690
|
+
* ★ It MUST NOT set `PLAYER_NAME` or `CONDUCTOR` — either flips {@link resolvePiRole}
|
|
691
|
+
* to `'player'`, which would keep the board dormant (and self-bootstrap a player).
|
|
692
|
+
*/
|
|
693
|
+
function buildPiCommandCenterSpawn(opts) {
|
|
694
|
+
const { cmd, args } = (opts.resolveBinary ?? resolvePiInteractiveBinary)();
|
|
695
|
+
const env = {
|
|
696
|
+
...opts.temporalEnvVars,
|
|
697
|
+
[config_1.ENV.TASK_QUEUE]: opts.taskQueue,
|
|
698
|
+
[config_1.ENV.ENSEMBLE]: opts.ensemble,
|
|
699
|
+
[config_1.ENV.MISSION_CONTROL]: '1', // #729 A2 role opt-in → resolver picks 'command-center'
|
|
700
|
+
[config_1.ENV.NO_PPID_WATCHDOG]: '1', // launched detached by the transient CLI (mirrors the conductor)
|
|
701
|
+
...(opts.devMode ? { [config_1.ENV.DEV_MODE]: '1' } : {}),
|
|
702
|
+
...(opts.adminToken ? { [config_1.ENV.HTTP_ADMIN_TOKEN]: opts.adminToken } : {}),
|
|
703
|
+
...(opts.anthropicApiKey ? { ANTHROPIC_API_KEY: opts.anthropicApiKey } : {}),
|
|
704
|
+
};
|
|
705
|
+
// Deliberately NO ENV.PLAYER_NAME / ENV.CONDUCTOR — they'd flip the role to player.
|
|
706
|
+
return { cmd, args, env };
|
|
707
|
+
}
|
|
676
708
|
/**
|
|
677
709
|
* Resolve the path to the compiled copilot bridge adapter entry point.
|
|
678
710
|
* In dev (ts-node), returns a ts-node command; in production, returns the dist path.
|
package/dist/types.d.ts
CHANGED
|
@@ -238,22 +238,33 @@ 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 (#715).** `supervised` is the daemon-enforced
|
|
242
|
-
* boundary for the realistic threat: a prompt-injected agent. A
|
|
243
|
-
* can only *emit* tool-call requests — Pi routes every one to
|
|
244
|
-
* `tool_call` handler, which engages the gate (non-`low-risk
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* `closed` on any lookup failure — no-fail-open), so an engaging agent can't
|
|
249
|
-
* self-downgrade a supervised player out of fail-closed.
|
|
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.
|
|
250
248
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
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.
|
|
257
268
|
*
|
|
258
269
|
* **Post-restart window:** on daemon restart the in-memory ingest tokens are
|
|
259
270
|
* invalidated, so existing players' gate engagements are rejected (403) until a
|