toolroll 0.9.65 → 0.9.68
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 +93 -0
- package/README.md +20 -2
- package/dist/acp-client.js +1 -3
- package/dist/agent-board.d.ts +0 -1
- package/dist/agent-board.js +0 -1
- package/dist/agent-fence.d.ts +69 -2
- package/dist/agent-fence.js +294 -11
- package/dist/agentconfig.js +4 -1
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +46 -0
- package/dist/browser/terminal.js +134 -23
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -29
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +56 -37
- package/dist/canonical-json.d.ts +14 -0
- package/dist/canonical-json.js +36 -0
- package/dist/chat-actions.js +1 -2
- package/dist/chat-admission.d.ts +19 -0
- package/dist/chat-admission.js +22 -0
- package/dist/chat-channel.js +1 -1
- package/dist/chat-delivery.js +0 -1
- package/dist/chat-follow.d.ts +17 -0
- package/dist/chat-follow.js +32 -0
- package/dist/chat-messages.js +0 -1
- package/dist/chat-settings-page.d.ts +23 -0
- package/dist/chat-settings-page.js +41 -0
- package/dist/claim.js +3 -6
- package/dist/claude-print.d.ts +12 -0
- package/dist/claude-print.js +34 -0
- package/dist/cli-http.d.ts +0 -7
- package/dist/cli-http.js +0 -21
- package/dist/cli.d.ts +3 -10
- package/dist/cli.js +15 -24
- package/dist/coding-provider.js +2 -1
- package/dist/coding-workspace.js +1 -1
- package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
- package/dist/connect-cli.js +331 -0
- package/dist/containment.d.ts +0 -2
- package/dist/containment.js +2 -6
- package/dist/contracts/chat-actions.d.ts +0 -1
- package/dist/contracts/chat-content.d.ts +0 -4
- package/dist/contracts/chat-content.js +0 -2
- package/dist/contracts/chat-tables.d.ts +0 -1
- package/dist/contracts/cli.d.ts +0 -62
- package/dist/contracts/cli.js +0 -4
- package/dist/contracts/coding-workspace.js +1 -1
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/contract.js +1 -1
- package/dist/contracts/flow-card.d.ts +0 -1
- package/dist/contracts/flow-propose.d.ts +0 -1
- package/dist/contracts/flow-send.js +1 -1
- package/dist/contracts/gateway-tools.d.ts +0 -1
- package/dist/contracts/integration-metadata.js +1 -1
- package/dist/contracts/lead-tools.d.ts +0 -1
- package/dist/contracts/memory-pass.d.ts +0 -1
- package/dist/contracts/memory-pass.js +1 -1
- package/dist/contracts/plan.js +1 -1
- package/dist/contracts/project-knowledge.js +1 -1
- package/dist/contracts/project-skills.d.ts +0 -1
- package/dist/contracts/proof.js +1 -1
- package/dist/contracts/recipes.js +1 -1
- package/dist/contracts/scope.js +1 -1
- package/dist/contracts/scout-report.js +1 -1
- package/dist/contracts/teams-callback.d.ts +0 -1
- package/dist/converse.d.ts +0 -6
- package/dist/converse.js +0 -19
- package/dist/daemon.js +3 -2
- package/dist/decision.d.ts +1 -3
- package/dist/deprecations.d.ts +12 -4
- package/dist/deprecations.js +27 -4
- package/dist/desktop-update-recovery.js +1 -1
- package/dist/desktop-update.js +3 -2
- package/dist/discord-chat.js +4 -15
- package/dist/discord-settings.d.ts +2 -6
- package/dist/discord-settings.js +10 -38
- package/dist/discord.js +12 -23
- package/dist/evidence.d.ts +0 -1
- package/dist/evidence.js +0 -3
- package/dist/exec.d.ts +0 -9
- package/dist/exec.js +3 -81
- package/dist/flow-actions.js +0 -3
- package/dist/flow-draft.d.ts +1 -3
- package/dist/flow-draft.js +15 -28
- package/dist/flow-scripts.d.ts +1 -8
- package/dist/flow-scripts.js +0 -13
- package/dist/flow-send.js +1 -1
- package/dist/flow-share.d.ts +0 -2
- package/dist/flow-share.js +1 -6
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.d.ts +0 -1
- package/dist/flow-triggers.js +1 -2
- package/dist/flow-unattended.js +1 -12
- package/dist/flows.js +1 -1
- package/dist/health-cli.d.ts +2 -2
- package/dist/health-cli.js +1 -1
- package/dist/invoke.d.ts +18 -3
- package/dist/invoke.js +142 -17
- package/dist/is-record.d.ts +2 -0
- package/dist/is-record.js +2 -0
- package/dist/lead-doors.d.ts +1 -5
- package/dist/lead-doors.js +0 -4
- package/dist/lead-tools.js +3 -6
- package/dist/live-detail.d.ts +0 -1
- package/dist/live-detail.js +0 -6
- package/dist/live-terminal.d.ts +24 -13
- package/dist/live-terminal.js +81 -43
- package/dist/live.d.ts +0 -1
- package/dist/live.js +1 -2
- package/dist/mcp-core.js +0 -2
- package/dist/memory-pass.js +1 -1
- package/dist/model-catalog.js +1 -1
- package/dist/modes.js +1 -12
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +436 -745
- package/dist/password-bearer.d.ts +0 -2
- package/dist/password-bearer.js +0 -4
- package/dist/peek-cli.js +2 -1
- package/dist/peek.d.ts +2 -26
- package/dist/peek.js +2 -168
- package/dist/phase-routing.d.ts +0 -4
- package/dist/phase-routing.js +2 -21
- package/dist/planner-source.d.ts +2 -1
- package/dist/planner-source.js +2 -12
- package/dist/planner.js +2 -14
- package/dist/process-tree.d.ts +0 -2
- package/dist/process-tree.js +0 -10
- package/dist/project-delete-ui.d.ts +6 -0
- package/dist/project-delete-ui.js +20 -1
- package/dist/project-delete.d.ts +0 -2
- package/dist/project-delete.js +1 -3
- package/dist/project-learning.d.ts +0 -14
- package/dist/project-skills.d.ts +0 -1
- package/dist/project-skills.js +0 -14
- package/dist/project-tools.js +2 -10
- package/dist/provider.d.ts +0 -17
- package/dist/provider.js +0 -26
- package/dist/pulls.d.ts +0 -2
- package/dist/pulls.js +0 -9
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +81 -4
- package/dist/release.d.ts +7 -1
- package/dist/release.js +20 -7
- package/dist/remote-exec.d.ts +3 -2
- package/dist/remote-exec.js +3 -2
- package/dist/repos.d.ts +0 -5
- package/dist/repos.js +2 -10
- package/dist/repository-context.d.ts +0 -2
- package/dist/repository-context.js +0 -4
- package/dist/restart-certification.d.ts +0 -2
- package/dist/restart-certification.js +1 -5
- package/dist/result-follow-ups.d.ts +1 -4
- package/dist/result-follow-ups.js +4 -8
- package/dist/result-review.d.ts +0 -2
- package/dist/result-review.js +0 -2
- package/dist/retention.d.ts +0 -1
- package/dist/retention.js +0 -1
- package/dist/review-context.d.ts +0 -1
- package/dist/review-context.js +1 -12
- package/dist/reviewer.d.ts +0 -16
- package/dist/reviewer.js +0 -18
- package/dist/run-closeout.js +2 -1
- package/dist/scope.d.ts +13 -0
- package/dist/scope.js +11 -18
- package/dist/scout-browser.js +2 -1
- package/dist/serve.js +3 -2
- package/dist/server/chrome.d.ts +10 -1
- package/dist/server/chrome.js +45 -4
- package/dist/server/console.css +60 -137
- package/dist/server/flows.js +2 -2
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +5 -0
- package/dist/server/pages.js +2 -2
- package/dist/server/people-tokens.js +3 -4
- package/dist/server/remote.js +1 -6
- package/dist/server/render-chat.d.ts +9 -2
- package/dist/server/render-chat.js +53 -7
- package/dist/server/render-pages.d.ts +28 -3
- package/dist/server/render-pages.js +78 -9
- package/dist/server/render-results.d.ts +0 -6
- package/dist/server/render-results.js +3 -9
- package/dist/server/render-tasks.js +6 -6
- package/dist/server/route-table.d.ts +0 -4
- package/dist/server/route-table.js +1 -6
- package/dist/server/settings.js +43 -22
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +10 -7
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills.d.ts +0 -6
- package/dist/skills.js +0 -8
- package/dist/slack-chat.d.ts +1 -7
- package/dist/slack-chat.js +5 -18
- package/dist/slack-settings.d.ts +2 -6
- package/dist/slack-settings.js +10 -38
- package/dist/slack.js +12 -23
- package/dist/sleep.d.ts +2 -0
- package/dist/sleep.js +2 -0
- package/dist/store.d.ts +11 -74
- package/dist/store.js +36 -179
- package/dist/structured-output.d.ts +0 -1
- package/dist/structured-output.js +0 -1
- package/dist/subagent-work.d.ts +0 -2
- package/dist/subagent-work.js +0 -2
- package/dist/subagents.d.ts +1 -3
- package/dist/subagents.js +20 -33
- package/dist/subscription-chat.d.ts +1 -3
- package/dist/subscription-chat.js +2 -3
- package/dist/task-sizing.d.ts +1 -4
- package/dist/task-sizing.js +3 -10
- package/dist/teams-chat.js +4 -11
- package/dist/teams.js +12 -19
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.js +2 -2
- package/dist/telegram.js +1 -1
- package/dist/token-notices.d.ts +0 -11
- package/dist/token-notices.js +1 -5
- package/dist/tool-broker.d.ts +12 -0
- package/dist/tool-broker.js +66 -2
- package/dist/tool-secret-store.d.ts +0 -3
- package/dist/tool-secret-store.js +2 -11
- package/dist/toolroll-update.d.ts +0 -2
- package/dist/toolroll-update.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/verification-evidence.d.ts +0 -1
- package/dist/verification-evidence.js +0 -1
- package/dist/webhooks.d.ts +0 -1
- package/dist/webhooks.js +0 -1
- package/dist/workspace-ui.d.ts +0 -8
- package/dist/workspace-ui.js +1 -6
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +5 -1
- package/dist/contracts/native-census.d.ts +0 -42
- package/dist/contracts/native-census.js +0 -36
- package/dist/process-recovery-anchor.d.ts +0 -75
- package/dist/process-recovery-anchor.js +0 -188
- package/dist/process-recovery-coalition.d.ts +0 -25
- package/dist/process-recovery-coalition.js +0 -105
- package/dist/process-recovery-eligibility.d.ts +0 -179
- package/dist/process-recovery-eligibility.js +0 -174
- package/dist/process-recovery-native.d.ts +0 -161
- package/dist/process-recovery-native.js +0 -595
- package/dist/process-recovery-provenance.d.ts +0 -163
- package/dist/process-recovery-provenance.js +0 -292
- package/dist/process-recovery-services.d.ts +0 -56
- package/dist/process-recovery-services.js +0 -191
- package/dist/process-recovery-settlement.d.ts +0 -32
- package/dist/process-recovery-settlement.js +0 -133
- package/dist/process-recovery.d.ts +0 -41
- package/dist/process-recovery.js +0 -91
- package/dist/session-brief.d.ts +0 -8
- package/dist/session-brief.js +0 -24
- package/dist/session-cli.d.ts +0 -42
- package/dist/session-cli.js +0 -271
- package/dist/session-contract.d.ts +0 -183
- package/dist/session-contract.js +0 -122
- package/dist/session-remote.d.ts +0 -19
- package/dist/session-remote.js +0 -86
- package/dist/session-service.d.ts +0 -22
- package/dist/session-service.js +0 -157
- package/dist/team-cli.js +0 -692
package/dist/invoke.js
CHANGED
|
@@ -29,19 +29,16 @@ import { witnessedRunner } from "./process-custody.js";
|
|
|
29
29
|
import { underStopWatch } from "./task-control.js";
|
|
30
30
|
import { childDatabaseEnv, isolatedChildDatabase, removeChildDatabase as removeAgentDatabase } from "./child-database.js";
|
|
31
31
|
import { RunSignInError, noToolsArgs, prepareRunTools } from "./project-tools.js";
|
|
32
|
-
import { mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
33
|
-
import { homedir } from "node:os";
|
|
32
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
33
|
+
import { homedir, tmpdir } from "node:os";
|
|
34
34
|
import { dirname, join } from "node:path";
|
|
35
35
|
import { poolRootOf } from "./worktree.js";
|
|
36
36
|
import { BRIDGE_DIR } from "./agent-fence.js";
|
|
37
|
-
import { agentFence, CheckoutBindingError, trustedWorktreeBinding, checkoutGitEnv, HARDENED_GIT_ARGS, codexFenceArgv, fenceGrants, fencedGitEnv, linuxFenceAvailable, linuxFenced, macosFenceAvailable, macosFenced, NO_GRANTS, runFolders, taskBoundary } from "./agent-fence.js";
|
|
37
|
+
import { agentFence, buildCacheEnv, buildCacheFolder, grantFolder, writeAllowlist, WRITES_DEFAULT, CheckoutBindingError, trustedWorktreeBinding, checkoutGitEnv, HARDENED_GIT_ARGS, codexFenceArgv, fenceGrants, fencedGitEnv, linuxFenceAvailable, linuxFenced, macosFenceAvailable, macosFenced, NO_GRANTS, runFolders, taskBoundary } from "./agent-fence.js";
|
|
38
38
|
import { proxyEnv, providerOnlyNetwork, startEgressProxy } from "./egress.js";
|
|
39
39
|
import { bridgeFits, startEgressBridge } from "./egress-bridge.js";
|
|
40
40
|
import { claudeConfigDir, claudeDefaults, codexDefaults, codexHomeDir, isolatedCodexHome } from "./provider-home.js";
|
|
41
41
|
import { billingOf, claudeBillingFrom, seenBilling } from "./spend.js";
|
|
42
|
-
/** The claude binary name — kept for the legacy quota rows and tests that
|
|
43
|
-
* describe history; new code carries a resolved AgentSpec instead. */
|
|
44
|
-
export const PROVIDER_BINARY = "claude";
|
|
45
42
|
/** "Within seconds": how soon a silent failed run must end for its stderr to
|
|
46
43
|
* be read for the sign-in signal. */
|
|
47
44
|
const AUTH_EARLY_EXIT_MS = 30_000;
|
|
@@ -64,6 +61,11 @@ function claudeSettingsAuthProblem(configDir) {
|
|
|
64
61
|
catch (error) {
|
|
65
62
|
if (error.code === "ENOENT")
|
|
66
63
|
return null;
|
|
64
|
+
// A sandbox around the plane (a project check's fence, say) may hide the file while the CLI still signs in from its
|
|
65
|
+
// keychain. Isolated builds never load it, and a login that needed it fails within seconds and is reported as one.
|
|
66
|
+
const code = error.code;
|
|
67
|
+
if (code === "EPERM" || code === "EACCES")
|
|
68
|
+
return null;
|
|
67
69
|
return "Can't read your Claude settings.json to check sign-in. Restore access to that file and try again. Isolated builds do not load it.";
|
|
68
70
|
}
|
|
69
71
|
let settings;
|
|
@@ -239,7 +241,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
239
241
|
// re-supply survives resolveChildEnv's chat-key strip); in
|
|
240
242
|
// "subscription" mode the key is STRIPPED from the child so the CLI's
|
|
241
243
|
// own login is used — its key is retained, just not handed over.
|
|
242
|
-
const { runner, clock: _clock, versionProbe: _probe, keyHome, timeoutMs: _hardTimeout, idleTimeoutMs: _idleTimeout, stdin: _callerStdin, accumulateUsage: _accumulateUsage, liveInput: _liveInput, acpCheck: _acpCheck, network: launchNetwork, ...runOptions } = options;
|
|
244
|
+
const { runner, clock: _clock, versionProbe: _probe, keyHome, timeoutMs: _hardTimeout, idleTimeoutMs: _idleTimeout, stdin: _callerStdin, accumulateUsage: _accumulateUsage, liveInput: _liveInput, acpCheck: _acpCheck, network: launchNetwork, writes: launchWrites, ...runOptions } = options;
|
|
243
245
|
// The live setting is read STRICTLY (atomic authority closure): a
|
|
244
246
|
// present mode file that says neither word is a value-shaped refusal
|
|
245
247
|
// before any stamp or process — never the subscription default a
|
|
@@ -300,12 +302,21 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
300
302
|
// The project's tools (v80): exactly its MCP servers, and none of the
|
|
301
303
|
// operator's global or a repository's own, served by this launch's broker
|
|
302
304
|
// (tool-broker.ts), which alone holds their secrets. A review keeps its isolation.
|
|
305
|
+
// Set once the fence is known: a fenced build's servers never start outside a fence.
|
|
306
|
+
let serverMustBeFenced = false;
|
|
307
|
+
// What the build writes (its checkout, its project's caches): a tool server never runs from there, nor reads it at all.
|
|
308
|
+
const serverUntrusted = workspace === null ? [] : toolUntrusted(store, workspace, keyHome);
|
|
303
309
|
const launchTools = invocation.phase === "review" ? null : await runTools(store, runId, spec, options.keyHome, clock, invocation.readOnlyTools, invocation.researchWithheld, containment, {
|
|
304
310
|
env: { ...process.env, ...(runOptions.env ?? {}) },
|
|
305
311
|
omitEnv: [...(runOptions.omitEnv ?? []), ...ALL_CREDENTIAL_ENV],
|
|
306
312
|
pass: childDatabaseEnv(isolatedDb.file),
|
|
307
|
-
|
|
313
|
+
// A tool server starts in a folder the build can't write, and never runs the build's own bytes (arc 2).
|
|
314
|
+
cwd: keyHome ?? homedir(),
|
|
315
|
+
untrusted: serverUntrusted,
|
|
308
316
|
wrap: () => serverFence,
|
|
317
|
+
// A fenced build's tool servers start only in a fence too, unless an instance operator allowed unconfined agents.
|
|
318
|
+
unwrapped: () => serverMustBeFenced && !store.unfencedAgentsAllowed()
|
|
319
|
+
? "This machine can't confine the tool's server (no macOS sandbox, no working bubblewrap), so it was not started." : null,
|
|
309
320
|
});
|
|
310
321
|
if (launchTools !== null && "refused" in launchTools) {
|
|
311
322
|
removeAgentDatabase(isolatedDb.dir);
|
|
@@ -333,7 +344,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
333
344
|
// tool servers outside its command sandbox, so its own launch folder is
|
|
334
345
|
// fenced too; Claude reads its config from there, inside the sandbox.
|
|
335
346
|
const ownTools = tools?.privateDir === undefined ? null : realpathSync(tools.privateDir);
|
|
336
|
-
const fenceOf = invocation.phase === "review" ? { fence: [], fileTools: [], grants: NO_GRANTS, gitPaths: [], problem: undefined } : runFence(store, runId, spec.provider, options.keyHome, codexHarness ? null : ownTools);
|
|
347
|
+
const fenceOf = invocation.phase === "review" ? { fence: [], fileTools: [], grants: NO_GRANTS, gitPaths: [], cache: undefined, problem: undefined } : runFence(store, runId, spec.provider, options.keyHome, codexHarness ? null : ownTools, launchWrites ?? WRITES_DEFAULT);
|
|
337
348
|
if (fenceOf.problem !== undefined) {
|
|
338
349
|
tools?.cleanup();
|
|
339
350
|
codexHome?.cleanup();
|
|
@@ -372,12 +383,16 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
372
383
|
// A tool server the broker starts is fenced by what this platform supports, whatever the provider: Codex's own
|
|
373
384
|
// profile fences only its agent's commands, and the broker starts servers outside it.
|
|
374
385
|
if (fence.length > 0) {
|
|
375
|
-
// A tool server keeps its own reach (it holds its tool's secrets, not the agent's code): never the agent's network
|
|
376
|
-
|
|
386
|
+
// A tool server keeps its own reach (it holds its tool's secrets, not the agent's code): never the agent's network,
|
|
387
|
+
// nor its write allowlist (a server keeps its own state where it always has).
|
|
388
|
+
const serverGrants = () => { const { network: _agentOnly, writes: _agentWrites, ...rest } = grants; return rest; };
|
|
389
|
+
// Nor read what the build wrote: however its arguments are spelled, no byte of the checkout or caches can load.
|
|
390
|
+
const serverDenies = [...fence, ...serverUntrusted.filter(path => existsSync(path)).map(path => realpathSync(path))];
|
|
377
391
|
if (macosFenceAvailable())
|
|
378
|
-
serverFence = (file, args) => macosFenced(file, args,
|
|
392
|
+
serverFence = (file, args) => macosFenced(file, args, serverDenies, serverGrants());
|
|
379
393
|
else if (linuxFenceAvailable())
|
|
380
|
-
serverFence = (file, args) => linuxFenced(file, args,
|
|
394
|
+
serverFence = (file, args) => linuxFenced(file, args, serverDenies, serverGrants());
|
|
395
|
+
serverMustBeFenced = true;
|
|
381
396
|
}
|
|
382
397
|
// No silent fallback (arc 1): a build this machine can't confine at the OS — Claude reduced to its own file-tool
|
|
383
398
|
// rules, Gemini to nothing — doesn't start unless the instance operator allowed unconfined agents here. An injected
|
|
@@ -457,6 +472,10 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
457
472
|
...(egress === null || !("policy" in network) ? {} : proxyEnv(egress, network.policy.loopback)),
|
|
458
473
|
// Its own temp folder, where its tools' sockets may live (the only one its fence admits besides its checkout).
|
|
459
474
|
...(runTemp === null ? {} : { TMPDIR: runTemp, TMP: runTemp, TEMP: runTemp }),
|
|
475
|
+
// Claude's own working folder goes there too (it is otherwise the shared /tmp/claude-<uid>).
|
|
476
|
+
...(runTemp === null || spec.provider !== "claude" ? {} : { CLAUDE_CODE_TMPDIR: runTemp }),
|
|
477
|
+
// A workspace build's package managers keep their caches in its project's own folder (arc 2).
|
|
478
|
+
...(fenceOf.cache === undefined || grants.writes === undefined ? {} : buildCacheEnv(fenceOf.cache)),
|
|
460
479
|
// This key is deliberately last: no caller may point an agent back at
|
|
461
480
|
// the live control database through a generic RunOptions override.
|
|
462
481
|
...childDatabaseEnv(isolatedDb.file),
|
|
@@ -906,7 +925,7 @@ function linkedFor(store, worktree) {
|
|
|
906
925
|
/** The paths this run's agent may not reach: the state folder around the live database (but not its own worktree),
|
|
907
926
|
* ~/.toolroll (or its older name), its task boundary and other launches' private folders; what the fence gives back
|
|
908
927
|
* (FenceGrants); and what only Claude's file tools refuse. */
|
|
909
|
-
function runFence(store, runId, provider, keyHome, ownTools) {
|
|
928
|
+
function runFence(store, runId, provider, keyHome, ownTools, writes = WRITES_DEFAULT) {
|
|
910
929
|
try {
|
|
911
930
|
// A plane always has a database file; an in-memory store (a test) has nothing to fence.
|
|
912
931
|
const databaseFile = store.databaseFile();
|
|
@@ -921,22 +940,128 @@ function runFence(store, runId, provider, keyHome, ownTools) {
|
|
|
921
940
|
const binding = worktree === null || row === null ? null : trustedWorktreeBinding(row.repo, worktree, poolRoots);
|
|
922
941
|
const boundary = worktree === null ? null : taskBoundary({ worktree, provider, ...home, binding });
|
|
923
942
|
const folders = runFolders(ownTools);
|
|
943
|
+
// A build with a checkout writes only its workspace unless its approval said anywhere (arc 2): its checkout and
|
|
944
|
+
// Git data, its launch folder, its project's caches and the provider state its CLI must write.
|
|
945
|
+
const allowed = worktree === null || row === null || writes !== "workspace" ? null
|
|
946
|
+
: writeAllowlist({ provider, home: keyHome ?? homedir(), worktree, binding, launchFolder: ownTools, cache: buildCacheFolder(row.repo, keyHome ?? homedir()) });
|
|
947
|
+
const fence = agentFence({ databaseFile, worktree, ...home, boundary, binding, runFolders: folders, extra: allowed?.deny ?? [] });
|
|
948
|
+
const grants = fenceGrants({ databaseFile, worktree, boundary, runFolders: folders, linked: linkedFor(store, worktree), binding, ...home, poolRoots });
|
|
924
949
|
return {
|
|
925
|
-
fence
|
|
950
|
+
fence,
|
|
926
951
|
fileTools: boundary?.fileTools ?? [],
|
|
927
952
|
// The shared install is granted only as the plane linked it: this checkout's project and stamped setup.
|
|
928
953
|
// Every plane checkout pool is denied around this one: the plane's own beside the database, and the one this
|
|
929
954
|
// checkout was recorded in (a --pool elsewhere), so another project's worktree, present at launch, is out of reach. Native wrappers also cover later creations.
|
|
930
|
-
grants:
|
|
931
|
-
poolRoots }),
|
|
955
|
+
grants: allowed === null ? grants : { ...grants, writes: allowed.writes, readOnly: [...new Set([...grants.readOnly, ...allowed.readOnly])].sort() },
|
|
932
956
|
// The checkout's own Git data, which a private /tmp gives back beside the checkout (agent-fence.ts linuxFenced).
|
|
933
957
|
gitPaths: binding === null ? [] : [binding.gitdir, join(binding.main, ".git")],
|
|
958
|
+
...(allowed === null || row === null ? {} : { cache: buildCacheFolder(row.repo, keyHome ?? homedir()) }),
|
|
934
959
|
};
|
|
935
960
|
}
|
|
936
961
|
catch (error) {
|
|
937
962
|
return { fence: [], fileTools: [], grants: NO_GRANTS, problem: error instanceof CheckoutBindingError ? error.message : "The build fence could not be prepared." };
|
|
938
963
|
}
|
|
939
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* A build's setup and check commands, run in its fence (arc 2): the same denies as its agent's (Toolroll's state,
|
|
967
|
+
* credentials, other projects and runs), its write allowlist when its approval sealed one (its checkout and Git data,
|
|
968
|
+
* its project's caches, and a temp folder of the command's own), and its package managers pointed at those caches.
|
|
969
|
+
* Their network stays as it was. A machine without an OS fence runs them as before; a fence that can't be prepared
|
|
970
|
+
* (a checkout whose binding no longer proves) runs nothing.
|
|
971
|
+
*/
|
|
972
|
+
export function fencedCommands(store, runId, writes, inner, available = () => macosFenceAvailable() || linuxFenceAvailable()) {
|
|
973
|
+
// An injected runner (tests) starts no real command, as an injected agent starts no real agent (invokeAgent).
|
|
974
|
+
if (inner !== runCommand)
|
|
975
|
+
return inner;
|
|
976
|
+
return async (file, args, options) => {
|
|
977
|
+
// As a build itself: where nothing can confine them, they run only when an instance operator allowed unconfined agents.
|
|
978
|
+
if (!available()) {
|
|
979
|
+
if (store.unfencedAgentsAllowed())
|
|
980
|
+
return inner(file, args, options);
|
|
981
|
+
return { code: 1, stdout: "", stderr: "This machine can't confine the build's commands (no macOS sandbox, no working bubblewrap), so they didn't run. Install bubblewrap, or an instance operator allows unconfined agents with `toolroll config unconfined-agents allow`.\n", timedOut: false, notFound: false };
|
|
982
|
+
}
|
|
983
|
+
const own = realpathSync(mkdtempSync(join(tmpdir(), "so-cmd-")));
|
|
984
|
+
try {
|
|
985
|
+
const temp = join(own, "tmp");
|
|
986
|
+
mkdirSync(temp, { mode: 0o700 });
|
|
987
|
+
const fenced = runFence(store, runId, "command", undefined, own, writes);
|
|
988
|
+
if (fenced.problem !== undefined)
|
|
989
|
+
return { code: 1, stdout: "", stderr: `${fenced.problem}\n`, timedOut: false, notFound: false };
|
|
990
|
+
if (fenced.fence.length === 0)
|
|
991
|
+
return inner(file, args, options);
|
|
992
|
+
// A self-hosted check's own database (child-database.ts) is the command's to write too.
|
|
993
|
+
const database = options?.env?.["TOOLROLL_DB"];
|
|
994
|
+
const checkData = database === undefined || fenced.grants.writes === undefined ? null : grantFolder(dirname(database), false);
|
|
995
|
+
const grants = checkData === null ? fenced.grants : { ...fenced.grants, writes: { ...fenced.grants.writes, subpaths: [...fenced.grants.writes.subpaths, checkData] } };
|
|
996
|
+
return await inner(file, args, {
|
|
997
|
+
...options,
|
|
998
|
+
fence: fenced.fence,
|
|
999
|
+
fenceGrants: grants,
|
|
1000
|
+
env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(fenced.cache === undefined || fenced.grants.writes === undefined ? {} : buildCacheEnv(fenced.cache)) },
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
finally {
|
|
1004
|
+
rmSync(own, { recursive: true, force: true });
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* A project's checks run in a checkout of a result's commit made for them (result-follow-ups.ts, batch-checks.ts), in
|
|
1010
|
+
* the same fence as a build's commands: Toolroll's state, credentials, other projects and runs denied; under the
|
|
1011
|
+
* project's writes setting, only that checkout, its Git data, its own temp folder, the project's caches and its check
|
|
1012
|
+
* database writable. Where nothing can confine them, they run only when an instance operator allowed unconfined agents.
|
|
1013
|
+
*/
|
|
1014
|
+
export function fencedCheckoutCommands(store, repo, checkout, inner, available = () => macosFenceAvailable() || linuxFenceAvailable()) {
|
|
1015
|
+
if (inner !== runCommand)
|
|
1016
|
+
return inner;
|
|
1017
|
+
return async (file, args, options) => {
|
|
1018
|
+
if (!available()) {
|
|
1019
|
+
if (store.unfencedAgentsAllowed())
|
|
1020
|
+
return inner(file, args, options);
|
|
1021
|
+
return { code: 1, stdout: "", stderr: "This machine can't confine the project's checks (no macOS sandbox, no working bubblewrap), so they didn't run. Install bubblewrap, or an instance operator allows unconfined agents with `toolroll config unconfined-agents allow`.\n", timedOut: false, notFound: false };
|
|
1022
|
+
}
|
|
1023
|
+
const databaseFile = store.databaseFile();
|
|
1024
|
+
if (databaseFile === null)
|
|
1025
|
+
return inner(file, args, options);
|
|
1026
|
+
const own = realpathSync(mkdtempSync(join(tmpdir(), "so-cmd-")));
|
|
1027
|
+
try {
|
|
1028
|
+
const temp = join(own, "tmp");
|
|
1029
|
+
mkdirSync(temp, { mode: 0o700 });
|
|
1030
|
+
let binding;
|
|
1031
|
+
try {
|
|
1032
|
+
binding = trustedWorktreeBinding(repo, checkout);
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return { code: 1, stdout: "", stderr: "The check's checkout couldn't be bound to its project, so nothing ran.\n", timedOut: false, notFound: false };
|
|
1036
|
+
}
|
|
1037
|
+
const poolRoots = [join(dirname(databaseFile), "worktrees"), ...store.listWorktrees().map(record => poolRootOf(record.path, record.repo))];
|
|
1038
|
+
const boundary = taskBoundary({ worktree: checkout, provider: "command", binding });
|
|
1039
|
+
const folders = runFolders(own);
|
|
1040
|
+
const cache = buildCacheFolder(repo);
|
|
1041
|
+
const allowed = store.projectWrites(repo) !== "workspace" ? null : writeAllowlist({ provider: "command", home: homedir(), worktree: checkout, binding, launchFolder: own, cache });
|
|
1042
|
+
const database = options?.env?.["TOOLROLL_DB"];
|
|
1043
|
+
const checkData = allowed === null || database === undefined ? null : grantFolder(dirname(database), false);
|
|
1044
|
+
const base = fenceGrants({ databaseFile, worktree: checkout, boundary, runFolders: folders, linked: linkedFor(store, checkout), binding, poolRoots });
|
|
1045
|
+
const grants = allowed === null ? base
|
|
1046
|
+
: { ...base, readOnly: [...new Set([...base.readOnly, ...allowed.readOnly])].sort(), writes: { ...allowed.writes, subpaths: [...allowed.writes.subpaths, ...(checkData === null ? [] : [checkData])] } };
|
|
1047
|
+
return await inner(file, args, {
|
|
1048
|
+
...options,
|
|
1049
|
+
fence: agentFence({ databaseFile, worktree: checkout, boundary, binding, runFolders: folders, extra: allowed?.deny ?? [] }),
|
|
1050
|
+
fenceGrants: grants,
|
|
1051
|
+
env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(allowed === null ? {} : buildCacheEnv(cache)) },
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
finally {
|
|
1055
|
+
rmSync(own, { recursive: true, force: true });
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
/** What a build writes that a tool server must never run from: its checkout and its project's caches. A checkout with
|
|
1060
|
+
* no record never gets this far: its fence refuses the launch (runFence) before any tool server can start. */
|
|
1061
|
+
function toolUntrusted(store, workspace, keyHome) {
|
|
1062
|
+
const row = store.getWorktree(workspace);
|
|
1063
|
+
return [workspace, ...(row === null ? [] : [buildCacheFolder(row.repo, keyHome ?? homedir())])];
|
|
1064
|
+
}
|
|
940
1065
|
/** The operator's own model choices a build routed with no model keeps (provider-home.ts). */
|
|
941
1066
|
function inheritedDefaults(provider, env, home) {
|
|
942
1067
|
if (provider === "claude")
|
package/dist/lead-doors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SharedActionOptions } from './chat-actions.js';
|
|
2
|
-
import { type
|
|
2
|
+
import { type LeadProposal, type Store } from "./store.js";
|
|
3
3
|
import type { VerifiedApprover } from "./principal.js";
|
|
4
4
|
import { type TeamChatProviderResolver } from './team-chat-authorization.js';
|
|
5
5
|
export type ProposalKind = LeadProposal["kind"];
|
|
@@ -78,7 +78,3 @@ export declare function dismissLeadProposal(store: Store, who: VerifiedApprover,
|
|
|
78
78
|
*/
|
|
79
79
|
export declare function confirmCoordinatorProposal(store: Store, who: VerifiedApprover, proposalId: number, now: Date, options: DoorOptions): DoorOutcome;
|
|
80
80
|
export declare function dismissCoordinatorProposal(store: Store, who: VerifiedApprover, proposalId: number, now: Date): boolean;
|
|
81
|
-
/** Which door renders a coordinator proposal for this reader: the repo must be admitted. */
|
|
82
|
-
export declare function coordinatorProposalVisible(who: {
|
|
83
|
-
repos: readonly string[];
|
|
84
|
-
}, proposal: CoordinatorProposal): boolean;
|
package/dist/lead-doors.js
CHANGED
|
@@ -260,10 +260,6 @@ export function dismissCoordinatorProposal(store, who, proposalId, now) {
|
|
|
260
260
|
return store.casCoordinatorProposal(proposalId, "pending", "dismissed", who.name, null, now);
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
|
-
/** Which door renders a coordinator proposal for this reader: the repo must be admitted. */
|
|
264
|
-
export function coordinatorProposalVisible(who, proposal) {
|
|
265
|
-
return who.repos.includes(proposal.repo);
|
|
266
|
-
}
|
|
267
263
|
/**
|
|
268
264
|
* The shared executor: one kind, one payload, the operator's own act
|
|
269
265
|
* through the primitive that owns it. Module-private (v3 review, finding
|
package/dist/lead-tools.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { sweepLine, sweepTable } from "./lead-sweep.js";
|
|
2
|
-
import { TEXT_LIMITS } from "./text-limits.js";
|
|
3
2
|
import { connectionsOf, oneClickOf } from "./mcp-connect.js";
|
|
4
3
|
import { checkLevelFromWords, isCheckLevel } from "./check-levels.js";
|
|
5
4
|
import { policyParts } from "./policy.js";
|
|
@@ -39,9 +38,9 @@ import { CHAT_TASK_ACTIONS, chatTaskRun, chatTaskStamp, isChatTaskAction } from
|
|
|
39
38
|
import { Buffer } from "node:buffer";
|
|
40
39
|
import { LEAD_ASK_OTHER } from "./store.js";
|
|
41
40
|
import { hasDisguisedText, hasForbiddenControls } from "./decision.js";
|
|
42
|
-
import { readVerifiedArtifact,
|
|
41
|
+
import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
|
|
43
42
|
import { parseAcceptanceCriteria } from "./scope.js";
|
|
44
|
-
import { diagnoseTaskDispatch
|
|
43
|
+
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
45
44
|
import { agentChoicesFor, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
46
45
|
import { isNewModel, modelWords, priceWords, runtimeStates, seenModels } from "./model-catalog.js";
|
|
47
46
|
import { agentsSummary, chosenWords, PHASES, postureWords, routeProblems, sameSpec, sizeSourceWords, sizeWords, specWords } from "./phase-routing.js";
|
|
@@ -55,7 +54,7 @@ import { planInChat } from "./chat-decide.js";
|
|
|
55
54
|
import { flowDecisionAt } from "./telegram-flow.js";
|
|
56
55
|
import { flowPersonOf } from "./flow-send.js";
|
|
57
56
|
import { flowInsights } from "./flow-insights.js";
|
|
58
|
-
import { describeTrigger,
|
|
57
|
+
import { describeTrigger, triggerConfigOf } from "./flow-triggers.js";
|
|
59
58
|
export const LEAD_MAX_PROPOSALS_PER_TURN = 5;
|
|
60
59
|
export { reportSummaryFor } from "./report-summary.js";
|
|
61
60
|
import { reportSummaryFor } from "./report-summary.js";
|
|
@@ -63,8 +62,6 @@ import { labelOf, nameOf, summaryOf, zonesOf } from "./subagent-admin.js";
|
|
|
63
62
|
import { SUBAGENT_TEMPLATES, withSection } from "./subagents.js";
|
|
64
63
|
import { callOutcome, callWords, defaultRule, ruleWords } from "./subagent-tools.js";
|
|
65
64
|
import { deskOf, routinesOf } from "./subagent-desk.js";
|
|
66
|
-
import { KITS } from "./kits.js";
|
|
67
|
-
import { STARTER_IDS } from "./flow-starters.js";
|
|
68
65
|
import { undoFor, weekOf, weekWords } from "./subagent-week.js";
|
|
69
66
|
import { PERSON_ID, personEntry, teamRoomOf } from "./lead-people.js";
|
|
70
67
|
import { aboutYouOf, checkAboutYouLine } from "./lead-about.js";
|
package/dist/live-detail.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export type LiveDetailSetting = {
|
|
|
10
10
|
};
|
|
11
11
|
/** Whether this project's live conversations show commands. Unknown or absent is off. */
|
|
12
12
|
export declare function liveCommandsShown(store: Store, repo: string | null): boolean;
|
|
13
|
-
export declare function liveDetailSetting(store: Store, repo: string): LiveDetailSetting | null;
|
|
14
13
|
/** Save the setting and its ledger line in one transaction. The caller has proved who is asking (and, to turn it on, their password). */
|
|
15
14
|
export declare function setLiveCommands(store: Store, change: {
|
|
16
15
|
repo: string;
|
package/dist/live-detail.js
CHANGED
|
@@ -26,12 +26,6 @@ export function liveCommandsShown(store, repo) {
|
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
export function liveDetailSetting(store, repo) {
|
|
30
|
-
const row = store.handle.prepare("SELECT repo, commands, updated_by, updated_at FROM live_detail_setting WHERE repo = ?").get(repo);
|
|
31
|
-
if (row === undefined)
|
|
32
|
-
return null;
|
|
33
|
-
return { repo: String(row["repo"]), commands: Number(row["commands"]) === 1, updatedBy: String(row["updated_by"]), updatedAt: String(row["updated_at"]) };
|
|
34
|
-
}
|
|
35
29
|
/** Save the setting and its ledger line in one transaction. The caller has proved who is asking (and, to turn it on, their password). */
|
|
36
30
|
export function setLiveCommands(store, change, now) {
|
|
37
31
|
store.transact(() => {
|
package/dist/live-terminal.d.ts
CHANGED
|
@@ -6,8 +6,6 @@ export declare const MAX_LIVE_TERMINALS = 8;
|
|
|
6
6
|
export declare const MAX_KEPT_TERMINALS = 16;
|
|
7
7
|
/** At most this many pages attached to one session (a desktop, a phone, a spare tab or two). */
|
|
8
8
|
export declare const MAX_VIEWERS = 4;
|
|
9
|
-
/** Raw output kept per session for a page that attaches later. */
|
|
10
|
-
export declare const SCROLLBACK_BYTES: number;
|
|
11
9
|
/** After SIGHUP, the terminal's processes have this long before SIGKILL. */
|
|
12
10
|
export declare const CLOSE_GRACE_MS = 2000;
|
|
13
11
|
/** Typed input not yet read by the program, per terminal: past this, more is refused until it reads. */
|
|
@@ -54,15 +52,24 @@ export declare const ttyName: (path: unknown) => string | null;
|
|
|
54
52
|
export declare function pendingInputOf(pty: unknown): number;
|
|
55
53
|
/** node-pty, if this install has it (an optional dependency with prebuilt binaries); null when it doesn't load. */
|
|
56
54
|
export declare function loadPtySpawn(): PtySpawn | null;
|
|
57
|
-
/**
|
|
58
|
-
export declare
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
/** How many lines above the screen a terminal keeps for a page that attaches later. */
|
|
56
|
+
export declare const SCREEN_SCROLLBACK_LINES = 5000;
|
|
57
|
+
/**
|
|
58
|
+
* The terminal as a page would see it now: every byte the program writes is parsed by a headless xterm here, so a page
|
|
59
|
+
* that attaches later gets the screen, its scrollback and its modes (the alternate screen of a full-screen program, the
|
|
60
|
+
* cursor, colours, bracketed paste, mouse reporting) — never a replay of raw output cut at an arbitrary byte, which
|
|
61
|
+
* could split an escape sequence or redraw a full-screen program garbled.
|
|
62
|
+
*/
|
|
63
|
+
export declare class ScreenMirror {
|
|
64
|
+
private readonly lines;
|
|
65
|
+
private readonly terminal;
|
|
66
|
+
private readonly serializer;
|
|
67
|
+
constructor(cols: number, rows: number, lines?: number);
|
|
68
|
+
write(data: string): void;
|
|
69
|
+
resize(cols: number, rows: number): void;
|
|
70
|
+
/** The screen as of this call (after everything written before it), handed to `done` once parsed. */
|
|
71
|
+
snapshot(done: (screen: string) => void): void;
|
|
72
|
+
dispose(): void;
|
|
66
73
|
}
|
|
67
74
|
/** One attached page. The host never waits on it. */
|
|
68
75
|
export type TerminalViewer = {
|
|
@@ -138,7 +145,8 @@ export type TerminalHostOptions = {
|
|
|
138
145
|
/** Start, close and remove, for the event log (never input or output). */
|
|
139
146
|
record?: (event: "start" | "close" | "remove" | "exit", session: TerminalSummary) => void;
|
|
140
147
|
maxSessions?: number;
|
|
141
|
-
|
|
148
|
+
/** Lines above the screen a terminal keeps for a page that attaches later (SCREEN_SCROLLBACK_LINES). */
|
|
149
|
+
scrollbackLines?: number;
|
|
142
150
|
closeGraceMs?: number;
|
|
143
151
|
ledger?: TerminalLedger;
|
|
144
152
|
/** The process table (tests stand a failing one in); null when it can't be read. */
|
|
@@ -175,7 +183,10 @@ export declare class TerminalHost {
|
|
|
175
183
|
cols: number;
|
|
176
184
|
rows: number;
|
|
177
185
|
}, admit?: () => string | null): Promise<TerminalSummary>;
|
|
178
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Attach a page: it gets the screen as it is now (ScreenMirror), then `hello`, then live output — what the program
|
|
188
|
+
* writes meanwhile is held and sent after the screen, in order. Returns its detach.
|
|
189
|
+
*/
|
|
179
190
|
attach(id: string, viewer: TerminalViewer): () => void;
|
|
180
191
|
/** How many pages are attached now. */
|
|
181
192
|
viewerCount(id: string): number;
|
package/dist/live-terminal.js
CHANGED
|
@@ -13,14 +13,13 @@ import { existsSync, mkdirSync, readlinkSync, realpathSync, statSync } from "nod
|
|
|
13
13
|
import { delimiter, isAbsolute, join } from "node:path";
|
|
14
14
|
import { codingEnvironment } from "./coding-provider.js";
|
|
15
15
|
import { teardownGit, refusal } from "./teardown-git.js";
|
|
16
|
+
import { sleep } from "./sleep.js";
|
|
16
17
|
export const TERMINAL_PROGRAMS = ["claude", "codex", "shell"];
|
|
17
18
|
/** At most this many running sessions per console, and this many kept at all (ended ones count until removed). */
|
|
18
19
|
export const MAX_LIVE_TERMINALS = 8;
|
|
19
20
|
export const MAX_KEPT_TERMINALS = 16;
|
|
20
21
|
/** At most this many pages attached to one session (a desktop, a phone, a spare tab or two). */
|
|
21
22
|
export const MAX_VIEWERS = 4;
|
|
22
|
-
/** Raw output kept per session for a page that attaches later. */
|
|
23
|
-
export const SCROLLBACK_BYTES = 1024 * 1024;
|
|
24
23
|
/** After SIGHUP, the terminal's processes have this long before SIGKILL. */
|
|
25
24
|
export const CLOSE_GRACE_MS = 2_000;
|
|
26
25
|
/** Typed input not yet read by the program, per terminal: past this, more is refused until it reads. */
|
|
@@ -70,37 +69,45 @@ export function loadPtySpawn() {
|
|
|
70
69
|
return null;
|
|
71
70
|
}
|
|
72
71
|
}
|
|
73
|
-
/**
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
72
|
+
/** How many lines above the screen a terminal keeps for a page that attaches later. */
|
|
73
|
+
export const SCREEN_SCROLLBACK_LINES = 5000;
|
|
74
|
+
const require = createRequire(import.meta.url);
|
|
75
|
+
/**
|
|
76
|
+
* The terminal as a page would see it now: every byte the program writes is parsed by a headless xterm here, so a page
|
|
77
|
+
* that attaches later gets the screen, its scrollback and its modes (the alternate screen of a full-screen program, the
|
|
78
|
+
* cursor, colours, bracketed paste, mouse reporting) — never a replay of raw output cut at an arbitrary byte, which
|
|
79
|
+
* could split an escape sequence or redraw a full-screen program garbled.
|
|
80
|
+
*/
|
|
81
|
+
export class ScreenMirror {
|
|
82
|
+
lines;
|
|
83
|
+
terminal;
|
|
84
|
+
serializer;
|
|
85
|
+
constructor(cols, rows, lines = SCREEN_SCROLLBACK_LINES) {
|
|
86
|
+
this.lines = lines;
|
|
87
|
+
const { Terminal } = require("@xterm/headless");
|
|
88
|
+
const { SerializeAddon } = require("@xterm/addon-serialize");
|
|
89
|
+
this.terminal = new Terminal({ cols, rows, scrollback: lines, allowProposedApi: true });
|
|
90
|
+
this.serializer = new SerializeAddon();
|
|
91
|
+
this.terminal.loadAddon(this.serializer);
|
|
92
|
+
}
|
|
93
|
+
write(data) { this.terminal.write(data); }
|
|
94
|
+
resize(cols, rows) { try {
|
|
95
|
+
this.terminal.resize(cols, rows);
|
|
96
|
+
}
|
|
97
|
+
catch { /* sizes are clamped before this */ } }
|
|
98
|
+
/** The screen as of this call (after everything written before it), handed to `done` once parsed. */
|
|
99
|
+
snapshot(done) {
|
|
100
|
+
this.terminal.write("", () => { let screen = ""; try {
|
|
101
|
+
screen = this.serializer.serialize({ scrollback: this.lines });
|
|
100
102
|
}
|
|
103
|
+
catch {
|
|
104
|
+
screen = "";
|
|
105
|
+
} done(screen); });
|
|
106
|
+
}
|
|
107
|
+
dispose() { try {
|
|
108
|
+
this.terminal.dispose();
|
|
101
109
|
}
|
|
102
|
-
|
|
103
|
-
get length() { return this.size; }
|
|
110
|
+
catch { /* already */ } }
|
|
104
111
|
}
|
|
105
112
|
const NO_LEDGER = { reserve() { }, running() { }, release() { }, forget() { } };
|
|
106
113
|
export class TerminalError extends Error {
|
|
@@ -237,7 +244,7 @@ export class TerminalHost {
|
|
|
237
244
|
}
|
|
238
245
|
let resolveExited;
|
|
239
246
|
const session = { id, owner, generation, repo, program, branch, worktree, startedAt: this.clock().toISOString(), endedAt: null, exit: null, endedBy: null, cleanup: null,
|
|
240
|
-
pty, leader: pty.pid, tty: pty.tty, known: new Map(), exitedAt: null, worktreeReal: realpathSync(worktree), startedMs: Date.now(), residentsOnly: false, residentsSeen: new Map(), cols, rows,
|
|
247
|
+
pty, leader: pty.pid, tty: pty.tty, known: new Map(), exitedAt: null, worktreeReal: realpathSync(worktree), startedMs: Date.now(), residentsOnly: false, residentsSeen: new Map(), cols, rows, screen: new ScreenMirror(cols, rows, this.options.scrollbackLines), viewers: new Set(), closing: null,
|
|
241
248
|
exited: new Promise(resolve => { resolveExited = resolve; }) };
|
|
242
249
|
this.sessions.set(id, session);
|
|
243
250
|
this.ledger.running(id);
|
|
@@ -246,7 +253,7 @@ export class TerminalHost {
|
|
|
246
253
|
pty.onData(data => {
|
|
247
254
|
this.noteSoon();
|
|
248
255
|
const chunk = Buffer.from(data, "utf8");
|
|
249
|
-
session.
|
|
256
|
+
session.screen.write(data);
|
|
250
257
|
for (const viewer of session.viewers) {
|
|
251
258
|
try {
|
|
252
259
|
viewer.output(chunk);
|
|
@@ -280,21 +287,50 @@ export class TerminalHost {
|
|
|
280
287
|
this.ledger.release(id, null);
|
|
281
288
|
}
|
|
282
289
|
}
|
|
283
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* Attach a page: it gets the screen as it is now (ScreenMirror), then `hello`, then live output — what the program
|
|
292
|
+
* writes meanwhile is held and sent after the screen, in order. Returns its detach.
|
|
293
|
+
*/
|
|
284
294
|
attach(id, viewer) {
|
|
285
295
|
const session = this.sessions.get(id);
|
|
286
296
|
if (session === undefined)
|
|
287
297
|
throw new TerminalError("No such terminal.");
|
|
288
298
|
if (session.viewers.size >= MAX_VIEWERS)
|
|
289
299
|
throw new TerminalError(`This terminal is open in ${MAX_VIEWERS} places already. Close one of them.`);
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
300
|
+
const held = [];
|
|
301
|
+
let live = false;
|
|
302
|
+
const gate = {
|
|
303
|
+
output: data => { if (live)
|
|
304
|
+
viewer.output(data);
|
|
305
|
+
else
|
|
306
|
+
held.push({ output: data }); },
|
|
307
|
+
control: message => { if (live)
|
|
308
|
+
viewer.control(message);
|
|
309
|
+
else
|
|
310
|
+
held.push({ control: message }); },
|
|
311
|
+
};
|
|
312
|
+
session.viewers.add(gate);
|
|
313
|
+
session.screen.snapshot(screen => {
|
|
314
|
+
if (!session.viewers.has(gate))
|
|
315
|
+
return;
|
|
316
|
+
try {
|
|
317
|
+
if (screen !== "")
|
|
318
|
+
viewer.output(Buffer.from(screen, "utf8"));
|
|
319
|
+
viewer.control({ t: "hello", cols: session.cols, rows: session.rows, program: session.program, ended: session.endedAt !== null });
|
|
320
|
+
if (session.exit !== null && !held.some(one => "control" in one && one.control.t === "exit"))
|
|
321
|
+
viewer.control({ t: "exit", code: session.exit.code, signal: session.exit.signal });
|
|
322
|
+
for (const one of held) {
|
|
323
|
+
if ("output" in one)
|
|
324
|
+
viewer.output(one.output);
|
|
325
|
+
else
|
|
326
|
+
viewer.control(one.control);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch { /* the viewer's own close handles it */ }
|
|
330
|
+
held.length = 0;
|
|
331
|
+
live = true;
|
|
332
|
+
});
|
|
333
|
+
return () => { session.viewers.delete(gate); };
|
|
298
334
|
}
|
|
299
335
|
/** How many pages are attached now. */
|
|
300
336
|
viewerCount(id) { return this.sessions.get(id)?.viewers.size ?? 0; }
|
|
@@ -319,6 +355,7 @@ export class TerminalHost {
|
|
|
319
355
|
session.pty.resize(session.cols, session.rows);
|
|
320
356
|
}
|
|
321
357
|
catch { /* it exited between */ }
|
|
358
|
+
session.screen.resize(session.cols, session.rows);
|
|
322
359
|
}
|
|
323
360
|
noteTimer = null;
|
|
324
361
|
/** Activity (output, typing) usually means a command started: note processes shortly after, at most every 250 ms. */
|
|
@@ -368,7 +405,7 @@ export class TerminalHost {
|
|
|
368
405
|
}
|
|
369
406
|
async sweep(session) {
|
|
370
407
|
const grace = this.options.closeGraceMs ?? CLOSE_GRACE_MS;
|
|
371
|
-
const pause =
|
|
408
|
+
const pause = sleep;
|
|
372
409
|
const leaderGone = () => session.pty === null;
|
|
373
410
|
let readable = true;
|
|
374
411
|
const members = () => {
|
|
@@ -487,6 +524,7 @@ export class TerminalHost {
|
|
|
487
524
|
catch { /* gone */ }
|
|
488
525
|
}
|
|
489
526
|
session.viewers.clear();
|
|
527
|
+
session.screen.dispose();
|
|
490
528
|
const result = await (this.options.git ?? teardownGit())(session.repo, ["worktree", "remove", "--force", "--", session.worktree]);
|
|
491
529
|
this.options.record?.("remove", this.summary(session));
|
|
492
530
|
if (result.code !== 0 && existsSync(session.worktree)) {
|
package/dist/live.d.ts
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
import { type KeyState, type LiveActivityKind, type LiveRecord, type LiveRecorderOptions } from "./live-records.js";
|
|
17
17
|
export declare const LIVE_LINE_CAP: number;
|
|
18
18
|
export declare const LIVE_FILE_CAP: number;
|
|
19
|
-
export declare const LIVE_CAP_MARKER = "[the live view stopped at its size cap \u2014 the run record continues]";
|
|
20
19
|
export declare const LIVE_REDACTED_LINE = "[a credential-shaped line was redacted]";
|
|
21
20
|
/** Finalized runs keep their live file this long for the final drain. */
|
|
22
21
|
export declare const LIVE_RETAIN_MS: number;
|
package/dist/live.js
CHANGED
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { scanForSecrets } from "./evidence.js";
|
|
19
|
-
import { createLiveRecorder,
|
|
19
|
+
import { createLiveRecorder, parseLiveRecords, redactLiveLines, projectLiveText, serializeLiveRecord } from "./live-records.js";
|
|
20
20
|
export const LIVE_LINE_CAP = 2 * 1024;
|
|
21
21
|
export const LIVE_FILE_CAP = 4 * 1024 * 1024;
|
|
22
|
-
export const LIVE_CAP_MARKER = LIVE_CAP_WORDS;
|
|
23
22
|
export const LIVE_REDACTED_LINE = "[a credential-shaped line was redacted]";
|
|
24
23
|
/** Finalized runs keep their live file this long for the final drain. */
|
|
25
24
|
export const LIVE_RETAIN_MS = 24 * 60 * 60 * 1000;
|