@xingular/harness 1.2.0 → 1.3.1-unstable.1
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 +63 -0
- package/README.md +26 -17
- package/builtin/skills/plugin-authoring/SKILL.md +34 -0
- package/dist/cli.js +7362 -6239
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +16 -4
- package/dist/sdk/agents/profiles.js +75 -12
- package/dist/sdk/annotations/bridge.d.ts +2 -0
- package/dist/sdk/annotations/bridge.js +4 -0
- package/dist/sdk/annotations/capture.d.ts +44 -2
- package/dist/sdk/annotations/capture.js +104 -3
- package/dist/sdk/annotations/delivery.d.ts +24 -17
- package/dist/sdk/annotations/delivery.js +82 -49
- package/dist/sdk/annotations/inspect.js +7 -6
- package/dist/sdk/annotations/pageScript.js +4 -4
- package/dist/sdk/annotations/part.d.ts +0 -2
- package/dist/sdk/annotations/part.js +4 -33
- package/dist/sdk/annotations/queue.d.ts +2 -2
- package/dist/sdk/annotations/queue.js +12 -7
- package/dist/sdk/annotations/serverMethods.js +11 -7
- package/dist/sdk/annotations/store.d.ts +13 -21
- package/dist/sdk/annotations/store.js +47 -37
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/list.js +2 -0
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/links.d.ts +51 -0
- package/dist/sdk/core/links.js +155 -0
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/harness.d.ts +5 -0
- package/dist/sdk/harness.js +11 -6
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +3 -2
- package/dist/sdk/index.js +2 -1
- package/dist/sdk/plugins/builtin.js +14 -9
- package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
- package/dist/sdk/plugins/builtinRegistry.js +9 -3
- package/dist/sdk/plugins/discover.d.ts +7 -1
- package/dist/sdk/plugins/discover.js +41 -2
- package/dist/sdk/plugins/identity.js +1 -1
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +3 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +21 -4
- package/dist/sdk/plugins/plugins.d.ts +2 -0
- package/dist/sdk/plugins/plugins.js +1 -0
- package/dist/sdk/plugins/proposals.d.ts +17 -0
- package/dist/sdk/plugins/proposals.js +147 -0
- package/dist/sdk/plugins/types.d.ts +1 -1
- package/dist/sdk/plugins/types.js +1 -1
- package/dist/sdk/prompt/compile.d.ts +13 -4
- package/dist/sdk/prompt/compile.js +46 -20
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/server/methods.js +14 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessionState.d.ts +13 -0
- package/dist/sdk/sessions/sessionState.js +31 -0
- package/dist/sdk/sessions/sessions.d.ts +7 -0
- package/dist/sdk/sessions/sessions.js +73 -15
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +195 -55
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
- package/dist/sdk/terminal/opentui/theme.js +2 -0
- package/dist/sdk/terminal/piExtension.js +98 -15
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +16 -0
- package/dist/sdk/terminal/terminals.js +130 -26
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
- package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
- package/dist/sdk/terminal/tui/annotateSend.js +40 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +16 -11
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +12 -2
- package/dist/sdk/terminal/tui/dialogs.js +1 -0
- package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
- package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
- package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
- package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
- package/dist/sdk/tools/defaultSkills.d.ts +4 -0
- package/dist/sdk/tools/defaultSkills.js +20 -0
- package/dist/sdk/tools/definitions.d.ts +3 -1
- package/dist/sdk/tools/definitions.js +17 -1
- package/dist/sdk/tools/dispatch.js +12 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/sessionName.d.ts +9 -0
- package/dist/sdk/tools/sessionName.js +58 -11
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/sdk/world/world.d.ts +0 -2
- package/dist/terminal-opentui.js +3331 -1584
- package/dist/terminal-pi.js +421 -133
- package/docs/README.md +3 -0
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +73 -36
- package/docs/architecture/03-variants.md +1 -3
- package/docs/architecture/04-prompt.md +24 -13
- package/docs/architecture/06-plugins-and-capabilities.md +46 -10
- package/docs/architecture/07-tools-and-gate.md +49 -8
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +68 -16
- package/docs/architecture/11-terminal.md +94 -45
- package/docs/architecture/12-storage.md +1 -0
- package/docs/architecture/13-world-tools.md +8 -6
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +20 -18
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
- package/docs/architecture/proposals/README.md +11 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
- package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
- package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
- package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +16 -13
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/1.3.1-unstable.1.md +8 -0
- package/docs/releases/README.md +9 -6
- package/docs/terminal-tui.md +127 -65
- package/package.json +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
- package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
|
@@ -4,12 +4,28 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { accessSync, constants, existsSync, lstatSync, mkdtempSync, realpathSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { hostShell, shellArguments } from "./shell.js";
|
|
7
9
|
import { isWithinWorkspace, verifyWorkspaceRoot } from "./workspace.js";
|
|
8
10
|
const MARKER_PREFIX = '__XINGULAR_WORKSPACE_SANDBOX_STARTED_';
|
|
9
11
|
const bashWriteScope = new AsyncLocalStorage();
|
|
10
12
|
export function runWithWorkspaceWriteScope(scope, action) {
|
|
11
13
|
return bashWriteScope.run(scope, action);
|
|
12
14
|
}
|
|
15
|
+
/** Why a Windows command asks first: Windows has no write-boundary sandbox the harness can use. */
|
|
16
|
+
export const WINDOWS_UNSANDBOXED_REASON = 'Windows has no workspace write-boundary sandbox, so this command would run without one. It will not run until this one-call request is approved.';
|
|
17
|
+
const SANDBOX_BACKENDS = {
|
|
18
|
+
darwin: ['/usr/bin/sandbox-exec'],
|
|
19
|
+
linux: ['/usr/bin/bwrap', '/bin/bwrap']
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The operating system's write-boundary sandbox for this platform, or null when it is not
|
|
23
|
+
* installed. Bash cannot run without it, so readiness diagnostics report it before a command runs.
|
|
24
|
+
*/
|
|
25
|
+
export function workspaceSandboxBackend(platform = process.platform) {
|
|
26
|
+
const candidates = SANDBOX_BACKENDS[platform];
|
|
27
|
+
return candidates ? systemExecutable(candidates) : null;
|
|
28
|
+
}
|
|
13
29
|
function systemExecutable(candidates) {
|
|
14
30
|
for (const candidate of candidates) {
|
|
15
31
|
try {
|
|
@@ -29,6 +45,18 @@ function shellPath() {
|
|
|
29
45
|
throw new Error('The workspace write-boundary sandbox requires Bash, but no Bash executable was found.');
|
|
30
46
|
return shell;
|
|
31
47
|
}
|
|
48
|
+
function requireShell(resolve) {
|
|
49
|
+
const shell = resolve();
|
|
50
|
+
if (!shell) {
|
|
51
|
+
throw new Error(process.platform === 'win32'
|
|
52
|
+
? 'No shell was found: neither Git Bash nor PowerShell is available. Bash was not run.'
|
|
53
|
+
: 'No Bash executable was found. Bash was not run.');
|
|
54
|
+
}
|
|
55
|
+
return shell;
|
|
56
|
+
}
|
|
57
|
+
function unconfinedCommand(shell, command) {
|
|
58
|
+
return { executable: shell.executable, args: shellArguments(shell, command), marker: '' };
|
|
59
|
+
}
|
|
32
60
|
function sbplString(value) {
|
|
33
61
|
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n')}"`;
|
|
34
62
|
}
|
|
@@ -72,13 +100,13 @@ export function workspaceSandboxCommand(platform, workspaceRoot, cwd, command, a
|
|
|
72
100
|
return readOnlySandboxCommand(platform, realpathSync(readOnlyTempRoot), cwd, shell, wrappedCommand, marker);
|
|
73
101
|
const writeRoots = [...new Set([workspaceRoot, ...additionalWriteRoots].map((root) => realpathSync(root)))].sort((left, right) => left.length - right.length);
|
|
74
102
|
if (platform === 'darwin') {
|
|
75
|
-
const executable =
|
|
103
|
+
const executable = workspaceSandboxBackend(platform);
|
|
76
104
|
if (!executable)
|
|
77
105
|
throw new Error('Workspace write-boundary sandbox unavailable: sandbox-exec was not found. Bash was not run.');
|
|
78
106
|
return { executable, args: ['-p', macSandboxProfile(writeRoots), shell, '-c', wrappedCommand], marker };
|
|
79
107
|
}
|
|
80
108
|
if (platform === 'linux') {
|
|
81
|
-
const executable =
|
|
109
|
+
const executable = workspaceSandboxBackend(platform);
|
|
82
110
|
if (!executable)
|
|
83
111
|
throw new Error('Workspace write-boundary sandbox unavailable: install bubblewrap (bwrap). Bash was not run.');
|
|
84
112
|
return {
|
|
@@ -99,13 +127,13 @@ export function workspaceSandboxCommand(platform, workspaceRoot, cwd, command, a
|
|
|
99
127
|
}
|
|
100
128
|
function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand, marker) {
|
|
101
129
|
if (platform === 'darwin') {
|
|
102
|
-
const executable =
|
|
130
|
+
const executable = workspaceSandboxBackend(platform);
|
|
103
131
|
if (!executable)
|
|
104
132
|
throw new Error('Read-only Bash sandbox unavailable: sandbox-exec was not found. Bash was not run.');
|
|
105
133
|
return { executable, args: ['-p', macReadOnlyProfile(tempRoot), shell, '-c', wrappedCommand], marker };
|
|
106
134
|
}
|
|
107
135
|
if (platform === 'linux') {
|
|
108
|
-
const executable =
|
|
136
|
+
const executable = workspaceSandboxBackend(platform);
|
|
109
137
|
if (!executable)
|
|
110
138
|
throw new Error('Read-only Bash sandbox unavailable: install bubblewrap (bwrap). Bash was not run.');
|
|
111
139
|
return {
|
|
@@ -127,7 +155,12 @@ function readOnlySandboxCommand(platform, tempRoot, cwd, shell, wrappedCommand,
|
|
|
127
155
|
throw new Error(`Read-only Bash sandbox unavailable on ${platform}. Bash was not run.`);
|
|
128
156
|
}
|
|
129
157
|
function killProcess(child) {
|
|
130
|
-
if (child.pid && process.platform
|
|
158
|
+
if (child.pid && process.platform === 'win32') {
|
|
159
|
+
// Git Bash and PowerShell start their own children; end the whole tree.
|
|
160
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (child.pid) {
|
|
131
164
|
try {
|
|
132
165
|
process.kill(-child.pid, 'SIGKILL');
|
|
133
166
|
return;
|
|
@@ -270,9 +303,12 @@ export function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
270
303
|
if (!isWithinWorkspace(root, canonicalCwd))
|
|
271
304
|
throw new Error('The Bash working directory is outside the active project workspace.');
|
|
272
305
|
const scope = options.workspaceWriteScope?.() ?? bashWriteScope.getStore() ?? 'workspace';
|
|
306
|
+
const platform = options.platform ?? process.platform;
|
|
307
|
+
const shell = options.shell ?? hostShell;
|
|
273
308
|
// Nothing widens a read-only scope: no Full Permission bypass, no outside request.
|
|
274
309
|
if (scope === 'read-only') {
|
|
275
|
-
|
|
310
|
+
if (platform === 'win32')
|
|
311
|
+
throw new Error('Plan-mode Bash needs a read-only sandbox, which Windows does not have. Bash was not run.');
|
|
276
312
|
const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'harness-read-only-bash-')));
|
|
277
313
|
try {
|
|
278
314
|
const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command, [], tempRoot);
|
|
@@ -292,11 +328,15 @@ export function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
292
328
|
bypass = false;
|
|
293
329
|
}
|
|
294
330
|
if (bypass) {
|
|
295
|
-
const result = await runProcess(
|
|
331
|
+
const result = await runProcess(unconfinedCommand(requireShell(shell), command), canonicalCwd, execOptions, null);
|
|
296
332
|
return { exitCode: result.exitCode };
|
|
297
333
|
}
|
|
298
|
-
|
|
299
|
-
|
|
334
|
+
// Without a sandbox every Windows command is an outside request: a person approves it once.
|
|
335
|
+
if (scope === 'outside' || platform === 'win32') {
|
|
336
|
+
const resolved = requireShell(shell);
|
|
337
|
+
const reason = scope === 'outside'
|
|
338
|
+
? 'This command requests filesystem writes outside the active project workspace. It will not run until this one-call request is approved.'
|
|
339
|
+
: WINDOWS_UNSANDBOXED_REASON;
|
|
300
340
|
let approved = false;
|
|
301
341
|
try {
|
|
302
342
|
approved = await options.requestWorkspaceWriteScope?.({ command, cwd: canonicalCwd, workspaceRoot: root, reason }) ?? false;
|
|
@@ -306,12 +346,12 @@ export function createWorkspaceBashOperations(workspaceRoot, options = {}) {
|
|
|
306
346
|
}
|
|
307
347
|
if (!approved)
|
|
308
348
|
throw new Error(`${reason} Bash was not run.`);
|
|
309
|
-
|
|
310
|
-
|
|
349
|
+
if (scope === 'outside')
|
|
350
|
+
execOptions.onData(Buffer.from('\n[Outside-workspace writes approved for this command; running once.]\n'));
|
|
351
|
+
const result = await runProcess(unconfinedCommand(resolved, command), canonicalCwd, execOptions, null);
|
|
311
352
|
return { exitCode: result.exitCode };
|
|
312
353
|
}
|
|
313
354
|
assertNoExternalHardlinks(root);
|
|
314
|
-
const platform = options.platform ?? process.platform;
|
|
315
355
|
const spec = workspaceSandboxCommand(platform, root, canonicalCwd, command);
|
|
316
356
|
const result = await runProcess(spec, canonicalCwd, execOptions, spec.marker);
|
|
317
357
|
return { exitCode: result.exitCode };
|
|
@@ -7,6 +7,17 @@ export interface WorkspaceWriteAuthorization {
|
|
|
7
7
|
expectedContentHash?: string;
|
|
8
8
|
signal?: AbortSignal;
|
|
9
9
|
}
|
|
10
|
+
export interface AtomicWriteLaunch {
|
|
11
|
+
executable: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
env: NodeJS.ProcessEnv;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* How the atomic write helper starts. macOS and Linux run it inside the write-boundary sandbox.
|
|
17
|
+
* Windows has none, so Node runs the helper directly; the target was still canonicalized and
|
|
18
|
+
* authorized, and the rename replaces the directory entry rather than writing through an alias.
|
|
19
|
+
*/
|
|
20
|
+
export declare function atomicWriteLaunch(platform: NodeJS.Platform, root: string, additionalRoots: readonly string[]): AtomicWriteLaunch;
|
|
10
21
|
declare function writeWorkspaceFile(path: string, content: string): Promise<void>;
|
|
11
22
|
export declare const workspaceWriteOperations: {
|
|
12
23
|
mkdir: (_directory: string) => Promise<void>;
|
|
@@ -52,6 +52,29 @@ try {
|
|
|
52
52
|
throw error
|
|
53
53
|
}
|
|
54
54
|
`;
|
|
55
|
+
/**
|
|
56
|
+
* How the atomic write helper starts. macOS and Linux run it inside the write-boundary sandbox.
|
|
57
|
+
* Windows has none, so Node runs the helper directly; the target was still canonicalized and
|
|
58
|
+
* authorized, and the rename replaces the directory entry rather than writing through an alias.
|
|
59
|
+
*/
|
|
60
|
+
export function atomicWriteLaunch(platform, root, additionalRoots) {
|
|
61
|
+
if (platform === 'win32') {
|
|
62
|
+
// Node needs SystemRoot to start on Windows; keep only what the helper requires.
|
|
63
|
+
const env = {};
|
|
64
|
+
for (const name of ['SystemRoot', 'windir', 'TEMP', 'TMP', 'USERPROFILE']) {
|
|
65
|
+
if (process.env[name])
|
|
66
|
+
env[name] = process.env[name];
|
|
67
|
+
}
|
|
68
|
+
return { executable: process.execPath, args: ['-e', atomicWriteScript], env };
|
|
69
|
+
}
|
|
70
|
+
const command = `exec ${shellQuote(process.execPath)} -e ${shellQuote(atomicWriteScript)}`;
|
|
71
|
+
const spec = workspaceSandboxCommand(platform, root, root, command, additionalRoots);
|
|
72
|
+
return {
|
|
73
|
+
executable: spec.executable,
|
|
74
|
+
args: spec.args,
|
|
75
|
+
env: { PATH: '/usr/bin:/bin', ...(process.env.HOME ? { HOME: process.env.HOME } : {}) }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
55
78
|
function shellQuote(value) {
|
|
56
79
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
57
80
|
}
|
|
@@ -96,17 +119,12 @@ async function runAtomicWrite(authorization, path, content) {
|
|
|
96
119
|
throw new Error('The write authorization no longer matches the active workspace target.');
|
|
97
120
|
}
|
|
98
121
|
const additionalRoots = outside ? [nearestExistingDirectory(canonicalPath)] : [];
|
|
99
|
-
const
|
|
100
|
-
const spec = workspaceSandboxCommand(process.platform, root, root, command, additionalRoots);
|
|
101
|
-
const env = {
|
|
102
|
-
PATH: '/usr/bin:/bin',
|
|
103
|
-
...(process.env.HOME ? { HOME: process.env.HOME } : {})
|
|
104
|
-
};
|
|
122
|
+
const launch = atomicWriteLaunch(process.platform, root, additionalRoots);
|
|
105
123
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
106
|
-
const child = spawn(
|
|
124
|
+
const child = spawn(launch.executable, launch.args, {
|
|
107
125
|
cwd: root,
|
|
108
126
|
detached: process.platform !== 'win32',
|
|
109
|
-
env,
|
|
127
|
+
env: launch.env,
|
|
110
128
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
129
|
windowsHide: true
|
|
112
130
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { delimiter, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { basename, delimiter, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { hostShell, shellLabel } from "../core/shell.js";
|
|
5
|
+
import { createWorkspaceBashOperations, workspaceSandboxBackend } from "../core/workspaceSandbox.js";
|
|
6
|
+
import { resolveWorkspaceRoot } from "../core/workspace.js";
|
|
3
7
|
import { defaultAssets } from "../terminal/terminals.js";
|
|
4
8
|
import { loadPi, PINNED_PI_VERSION } from "../pi/runtime.js";
|
|
5
9
|
import { list as listPlugins } from "../plugins/api.js";
|
|
@@ -12,18 +16,23 @@ import { BRAVE, TAVILY, EXA } from "../web/search.js";
|
|
|
12
16
|
import { KEYLESS_SEARCH, WEB_PLUGIN } from "../web/tools.js";
|
|
13
17
|
const pass = (evidence) => ({ status: 'pass', evidence });
|
|
14
18
|
const fail = (evidence, remedy) => ({ status: 'fail', evidence, remedy });
|
|
15
|
-
function
|
|
19
|
+
function resolvedExecutable(name) {
|
|
16
20
|
const paths = isAbsolute(name) ? [name] : (process.env.PATH ?? '').split(delimiter).filter(Boolean).map(dir => join(dir, name));
|
|
17
|
-
|
|
21
|
+
for (const path of paths) {
|
|
18
22
|
try {
|
|
19
23
|
accessSync(path, constants.X_OK);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
+
if (statSync(path).isFile())
|
|
25
|
+
return path;
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
catch { /* try the next PATH entry */ }
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
26
30
|
}
|
|
31
|
+
function executable(name) {
|
|
32
|
+
return resolvedExecutable(name) !== null;
|
|
33
|
+
}
|
|
34
|
+
/** One command written by the harness, not the model: it prints a fixed word and touches nothing. */
|
|
35
|
+
const BASH_PROBE_COMMAND = `printf '%s\\n' harness-doctor`;
|
|
27
36
|
export async function builtinChecks(harness, context, secrets, assets = defaultAssets()) {
|
|
28
37
|
const check = (id, category, title, local) => ({ id, category, title, local });
|
|
29
38
|
const account = () => context.accountId ? harness.accounts.find(context.accountId) : null;
|
|
@@ -96,10 +105,60 @@ export async function builtinChecks(harness, context, secrets, assets = defaultA
|
|
|
96
105
|
}), tools: ['read', 'grep', 'find', 'ls', 'write', 'edit']
|
|
97
106
|
},
|
|
98
107
|
{
|
|
99
|
-
...check('native.bash', 'tools', 'Native shell tool', async () =>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
...check('native.bash', 'tools', 'Native shell tool', async () => {
|
|
109
|
+
if (process.platform === 'win32') {
|
|
110
|
+
const shell = hostShell();
|
|
111
|
+
if (!shell)
|
|
112
|
+
return fail('Neither Git Bash nor PowerShell was found.', 'Install Git for Windows, or restore Windows PowerShell.');
|
|
113
|
+
return { status: 'warning', evidence: `Commands run in ${shellLabel(shell)} at ${shell.executable}. Windows has no workspace write-boundary sandbox, so each command asks for approval unless Full Permission is on, and plan mode cannot run commands. No shell command was run.`, remedy: 'Approve each command when asked, or turn on Full Permission for this session.' };
|
|
114
|
+
}
|
|
115
|
+
const shell = resolvedExecutable('bash');
|
|
116
|
+
if (!shell)
|
|
117
|
+
return fail('Bash was not found on the inherited PATH.', 'Verify the shell executable used by Pi.');
|
|
118
|
+
// Bash runs inside the platform's write-boundary sandbox; without that backend every call
|
|
119
|
+
// fails before the command starts, so presence of bash alone is not readiness.
|
|
120
|
+
const backend = workspaceSandboxBackend(process.platform);
|
|
121
|
+
return backend
|
|
122
|
+
? pass(`Bash is executable at ${shell}, and the ${basename(backend)} workspace write-boundary sandbox it runs inside is present. No shell command was run.`)
|
|
123
|
+
: fail(`Bash is executable at ${shell}, but the workspace write-boundary sandbox this platform needs is missing, so every Bash call fails before it runs.`, process.platform === 'linux' ? 'Install bubblewrap (bwrap) so Bash can run inside the workspace boundary.' : 'Restore the platform sandbox executable so Bash can run inside the workspace boundary.');
|
|
124
|
+
}),
|
|
125
|
+
tools: ['bash'],
|
|
126
|
+
live: {
|
|
127
|
+
notice: 'Run one fixed command written by the harness (printf) in the working folder, inside the same workspace write-boundary sandbox as a normal Bash call. No model input, no network request, no file written; only the exit status is reported.',
|
|
128
|
+
run: async (probe) => {
|
|
129
|
+
if (process.platform === 'win32')
|
|
130
|
+
return { status: 'unverified', evidence: 'On Windows every command needs a person to approve it, so the diagnostic does not run one.' };
|
|
131
|
+
const refusal = await harness.sessions.diagnosticGate(probe.run, context.caller).check('bash', { command: BASH_PROBE_COMMAND });
|
|
132
|
+
if (refusal)
|
|
133
|
+
return { status: 'blocked', evidence: `The tool policy refused the diagnostic command: ${refusal}`, remedy: 'Review what the policy allows Bash to do in this session.' };
|
|
134
|
+
const toolCallId = randomUUID();
|
|
135
|
+
probe.run.emit('tool.started', { toolCallId, name: 'bash', summary: 'Synthetic diagnostic command' });
|
|
136
|
+
let output = '';
|
|
137
|
+
try {
|
|
138
|
+
// The same root, boundary, hard-link scan and sandbox a real Bash call takes. A
|
|
139
|
+
// diagnostic never widens that boundary, so Full Permission cannot change this result.
|
|
140
|
+
const workspaceRoot = resolveWorkspaceRoot(context.cwd);
|
|
141
|
+
const { exitCode } = await createWorkspaceBashOperations(workspaceRoot).exec(BASH_PROBE_COMMAND, workspaceRoot, {
|
|
142
|
+
onData: chunk => { if (output.length < 400)
|
|
143
|
+
output += chunk.toString('utf8'); },
|
|
144
|
+
signal: probe.signal,
|
|
145
|
+
timeout: 20
|
|
146
|
+
});
|
|
147
|
+
probe.run.emit('tool.completed', { toolCallId, name: 'bash', ok: exitCode === 0 });
|
|
148
|
+
return exitCode === 0 && output.includes('harness-doctor')
|
|
149
|
+
? pass('A real command ran in the working folder through the workspace sandbox and returned exit status 0 with the expected output.')
|
|
150
|
+
: fail(`The shell returned exit status ${exitCode ?? 'unknown'} for the diagnostic command.`, 'Review the shell executable, the workspace write-boundary sandbox and the working folder permissions.');
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
probe.run.emit('tool.completed', { toolCallId, name: 'bash', ok: false });
|
|
154
|
+
return {
|
|
155
|
+
status: 'blocked',
|
|
156
|
+
evidence: `The command could not start in this working folder, so the shell itself was not exercised: ${error instanceof Error ? error.message : String(error)}`,
|
|
157
|
+
remedy: 'Resolve what stops Bash before it runs (a hard-link alias in the workspace, a missing sandbox backend, or a redirected workspace root), or choose another working folder.'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
103
162
|
},
|
|
104
163
|
check('runtime.storage', 'runtime', 'State and data storage', async () => {
|
|
105
164
|
accessSync(harness.stateDir, constants.R_OK | constants.W_OK);
|
package/dist/sdk/harness.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Hub, type HubOptions } from "./hub/hub.js";
|
|
|
4
4
|
import { type SecretStore } from "./accounts/secrets.js";
|
|
5
5
|
import { Bus } from "./core/bus.js";
|
|
6
6
|
import { type Directory, type HarnessEnv } from "./core/env.js";
|
|
7
|
+
import { type LinkOpener } from "./core/links.js";
|
|
7
8
|
import { ConnectedApps } from "./mcp/service.js";
|
|
8
9
|
import type { AppOAuthClients } from "./mcp/types.js";
|
|
9
10
|
import { Lessons } from "./history/lessons.js";
|
|
@@ -69,6 +70,8 @@ export interface HarnessOptions {
|
|
|
69
70
|
hookImporter?: (address: string) => Promise<unknown>;
|
|
70
71
|
/** Where managed worktrees are made, the prefix of branches they get, and how many `prune` keeps. */
|
|
71
72
|
worktrees?: WorktreesOptions;
|
|
73
|
+
/** How a link a person clicked reaches the operating system. Defaults to the system's own opener. */
|
|
74
|
+
linkOpener?: LinkOpener;
|
|
72
75
|
}
|
|
73
76
|
/** What starting on a data root found and repaired. */
|
|
74
77
|
export interface Recovery {
|
|
@@ -99,6 +102,8 @@ export declare class Harness {
|
|
|
99
102
|
readonly env: Env;
|
|
100
103
|
readonly stateDir: string;
|
|
101
104
|
readonly policy: ToolPolicy;
|
|
105
|
+
/** How a link a person clicked reaches the operating system (page 10, `links.open`). */
|
|
106
|
+
readonly linkOpener: LinkOpener;
|
|
102
107
|
readonly accounts: Accounts;
|
|
103
108
|
readonly approvals: Approvals;
|
|
104
109
|
readonly runs: RunRegistry;
|
package/dist/sdk/harness.js
CHANGED
|
@@ -15,6 +15,7 @@ import { DEFAULT_PRODUCT_NAME, EMPTY_DIRECTORY } from "./core/env.js";
|
|
|
15
15
|
import { HarnessError } from "./core/errors.js";
|
|
16
16
|
import { readJsonOr } from "./core/fs.js";
|
|
17
17
|
import { closeIndex } from "./core/index.js";
|
|
18
|
+
import { openWithSystem } from "./core/links.js";
|
|
18
19
|
import { ConnectedApps } from "./mcp/service.js";
|
|
19
20
|
import { migrateConnectionScopes } from "./mcp/scopeMigration.js";
|
|
20
21
|
import { Lessons } from "./history/lessons.js";
|
|
@@ -26,6 +27,7 @@ import { VariantTrust } from "./variant/trust.js";
|
|
|
26
27
|
import * as notes from "./wiki/notes.js";
|
|
27
28
|
import * as pluginApi from "./plugins/api.js";
|
|
28
29
|
import * as pluginInstall from "./plugins/install.js";
|
|
30
|
+
import * as pluginProposals from "./plugins/proposals.js";
|
|
29
31
|
import * as attachments from "./plugins/attachments.js";
|
|
30
32
|
import { HARNESS_PLUGINS } from "./plugins/builtin.js";
|
|
31
33
|
import { World } from "./world/world.js";
|
|
@@ -72,6 +74,8 @@ export class Harness {
|
|
|
72
74
|
env;
|
|
73
75
|
stateDir;
|
|
74
76
|
policy;
|
|
77
|
+
/** How a link a person clicked reaches the operating system (page 10, `links.open`). */
|
|
78
|
+
linkOpener;
|
|
75
79
|
accounts;
|
|
76
80
|
approvals;
|
|
77
81
|
runs;
|
|
@@ -118,6 +122,7 @@ export class Harness {
|
|
|
118
122
|
this.env = new Env(options.dataRoot || null, options.directory ?? EMPTY_DIRECTORY, options.productName ?? DEFAULT_PRODUCT_NAME, this.bus);
|
|
119
123
|
this.hub = new Hub(this, options.hub);
|
|
120
124
|
this.policy = options.policy ?? {};
|
|
125
|
+
this.linkOpener = options.linkOpener ?? openWithSystem;
|
|
121
126
|
configureBuiltins(options.builtins ?? []);
|
|
122
127
|
this.trust = new VariantTrust(this.stateDir);
|
|
123
128
|
this.refineHost = options.refineHost ?? null;
|
|
@@ -189,7 +194,7 @@ export class Harness {
|
|
|
189
194
|
worktrees: this.worktrees
|
|
190
195
|
});
|
|
191
196
|
this.sideQuestions = new SideQuestions({ env: this.env, sessions: this.sessions, runs: this.runs });
|
|
192
|
-
this.world = new World({ root: () => this.env.root, stateDir: this.stateDir, secrets, browser: this.browserEnabled ? undefined : false
|
|
197
|
+
this.world = new World({ root: () => this.env.root, stateDir: this.stateDir, secrets, browser: this.browserEnabled ? undefined : false });
|
|
193
198
|
// The harness's own world tools come first; a tool an application provides under the same name is not added again.
|
|
194
199
|
this.sessions.provide(this.world.provider());
|
|
195
200
|
this.doctor = new Doctor({
|
|
@@ -200,7 +205,7 @@ export class Harness {
|
|
|
200
205
|
this.refine = new Refinements({ root: () => this.env.root, bus: this.bus, trust: this.trust, host: () => this.refineHost, runtime: refineRuntime });
|
|
201
206
|
this.lessons = new Lessons({ root: () => this.env.root, runtime: refineRuntime });
|
|
202
207
|
this.pageNotes = new NoteDeliveries({ root: () => this.env.root, bus: this.bus, terminals: () => this.terminals ?? null });
|
|
203
|
-
this.terminals = new Terminals({ trust: this.trust, subagents: this.subagents, sideQuestions: this.sideQuestions, resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }), env: this.env, stateDir: this.stateDir, accounts: this.accounts, runs: this.runs, approvals: this.approvals, apps: this.apps, policy: this.policy, bus: this.bus, plugins: this.plugins, usage: this.usage, usageLimits: this.usageLimits, pluginControl: { list: (scope) => pluginApi.list(this, scope), attach: (params, on) => pluginApi.attach(this, params, on), trust: (scope) => pluginApi.trust(this, scope), untrust: (scope) => pluginApi.untrust(this, scope), configure: (params) => pluginApi.configure(this, params), install: params => pluginInstall.install(this, params), remove: params => pluginInstall.remove(this, params) }, assets: options.assets, hub: this.hub, world: this.world, doctor: this.doctor, worktrees: this.worktrees, noteCapture: this.noteCapture, pageNotes: this.pageNotes });
|
|
208
|
+
this.terminals = new Terminals({ trust: this.trust, subagents: this.subagents, sideQuestions: this.sideQuestions, resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }), env: this.env, stateDir: this.stateDir, accounts: this.accounts, runs: this.runs, approvals: this.approvals, apps: this.apps, policy: this.policy, bus: this.bus, plugins: this.plugins, usage: this.usage, usageLimits: this.usageLimits, pluginControl: { list: (scope) => pluginApi.list(this, scope), attach: (params, on) => pluginApi.attach(this, params, on), trust: (scope) => pluginApi.trust(this, scope), untrust: (scope) => pluginApi.untrust(this, scope), configure: (params) => pluginApi.configure(this, params), install: params => pluginInstall.install(this, params), remove: params => pluginInstall.remove(this, params), proposals: { list: () => pluginProposals.list(this.env.root), preview: id => pluginProposals.preview(this.env.root, id), accept: (id, hash) => pluginProposals.accept(this, id, hash), reject: (id, hash) => pluginProposals.reject(this.env.root, id, hash) } }, assets: options.assets, hub: this.hub, world: this.world, doctor: this.doctor, worktrees: this.worktrees, noteCapture: this.noteCapture, pageNotes: this.pageNotes });
|
|
204
209
|
this.ready = this.apps.start().catch((error) => log('connected apps', error instanceof Error ? error.message : String(error)));
|
|
205
210
|
this.runs.listen((event, run) => {
|
|
206
211
|
this.accounts.onRunEvent(event, run);
|
|
@@ -215,9 +220,9 @@ export class Harness {
|
|
|
215
220
|
if (runEnded)
|
|
216
221
|
indexRunUsage(this.env.root, event.runId);
|
|
217
222
|
if (runEnded && run?.sessionId) {
|
|
218
|
-
// Notes sent
|
|
219
|
-
//
|
|
220
|
-
|
|
223
|
+
// Notes sent while this session was working go out now (runEnded waits for the terminal to
|
|
224
|
+
// see its run as ended, and tries again while it is still busy).
|
|
225
|
+
void this.pageNotes.runEnded(run.sessionId).catch((error) => log('page notes', error instanceof Error ? error.message : String(error)));
|
|
221
226
|
}
|
|
222
227
|
if ((event.type === 'completed' || event.type === 'failed') && run) {
|
|
223
228
|
if (event.type === 'completed')
|
|
@@ -294,7 +299,7 @@ export class Harness {
|
|
|
294
299
|
if (!sessionId || recoveredSessions.has(sessionId))
|
|
295
300
|
continue;
|
|
296
301
|
recoveredSessions.add(sessionId);
|
|
297
|
-
|
|
302
|
+
void this.pageNotes.runEnded(sessionId).catch((error) => log('page notes', error instanceof Error ? error.message : String(error)));
|
|
298
303
|
}
|
|
299
304
|
catch {
|
|
300
305
|
// A malformed run record does not prevent the rest of recovery.
|
package/dist/sdk/hub/hub.js
CHANGED
|
@@ -316,7 +316,7 @@ export class Hub {
|
|
|
316
316
|
attachments.save(root, choicesBefore);
|
|
317
317
|
if (entry.delivery.kind === 'pi-package')
|
|
318
318
|
for (const pluginId of receipt.resourceIds)
|
|
319
|
-
await packages.
|
|
319
|
+
await packages.removeDirect(this.harness, { pluginId });
|
|
320
320
|
throw error;
|
|
321
321
|
}
|
|
322
322
|
this.harness.bus.emit({ type: 'hub.changed' });
|
|
@@ -404,8 +404,10 @@ export class Hub {
|
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
if (receipt.entry.delivery.kind === 'pi-package')
|
|
407
|
-
for (const pluginId of receipt.resourceIds)
|
|
408
|
-
await
|
|
407
|
+
for (const pluginId of receipt.resourceIds) {
|
|
408
|
+
if ((await pluginApi.list(this.harness, {})).some((plugin) => plugin.id === pluginId && plugin.install))
|
|
409
|
+
await packages.removeDirect(this.harness, { pluginId });
|
|
410
|
+
}
|
|
409
411
|
writeJson(join(root, 'hub', 'installed.json'), records.filter((item) => item !== receipt));
|
|
410
412
|
committed = true;
|
|
411
413
|
try {
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { hubCatalogSchema, hubEntrySchema, hubOverviewSchema, hubReceiptSchema,
|
|
|
6
6
|
export { Bus, log, type Notice } from "./core/bus.js";
|
|
7
7
|
export { EMPTY_DIRECTORY, type Directory, type HarnessEnv } from "./core/env.js";
|
|
8
8
|
export { HarnessError, invalid, notFound, toErrorShape, type ErrorShape } from "./core/errors.js";
|
|
9
|
+
export { linkOpenCommand, linkTarget, openableTarget, openLink, openLinkTarget, openWithSystem, type LinkOpener, type LinkTarget, type OpenedTarget } from "./core/links.js";
|
|
9
10
|
export { findBinary, searchPath } from "./core/path.js";
|
|
10
11
|
export { fileSecretStore, memorySecretStore, secretKey, type SecretStore } from "./accounts/secrets.js";
|
|
11
12
|
export type { Account, AccountsOverview, Pool } from "./accounts/store.js";
|
|
@@ -16,7 +17,7 @@ export type { RunCapabilitySnapshot } from "./capabilities/runSnapshot.js";
|
|
|
16
17
|
export type { CallerContext } from "./core/caller.js";
|
|
17
18
|
export { dispatch as harnessCall } from "./tools/dispatch.js";
|
|
18
19
|
export { compile, render, SECTION_ORDER, CONTEXT_MESSAGE_TYPE, CONTEXT_SNAPSHOT_KEY, type ContextPack, type HostSection, type HostToolGroup, type Launch, type Reference, type Section } from "./prompt/compile.js";
|
|
19
|
-
export { runEventSchema, runRecordSchema, isActiveStatus, type
|
|
20
|
+
export { runEventSchema, runRecordSchema, isActiveStatus, type TodoItem, type RunEvent, type RunEventType, type RunRecord } from "./sessions/contracts.js";
|
|
20
21
|
export type { ApprovalFacts, ApprovalPolicy, AutomaticReason } from "./sessions/approvals.js";
|
|
21
22
|
export type { ScopeDecision, ToolCall, ToolPolicy } from "./sessions/policy.js";
|
|
22
23
|
export type { ActiveRun, ApprovalDecision, ApprovalInput, QuestionAnswer, QuestionInput } from "./sessions/registry.js";
|
|
@@ -30,7 +31,7 @@ export type { Effort } from "./sessions/runner.js";
|
|
|
30
31
|
export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE, type HarnessCall, type HarnessToolDefinition } from "./tools/definitions.js";
|
|
31
32
|
export type { ToolProvider, ToolResult, ToolSession, ToolSpec } from "./tools/types.js";
|
|
32
33
|
export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from "./tools/spawnSubagent.js";
|
|
33
|
-
export { CHECKLIST_WRITE, checklistOf } from "./tools/
|
|
34
|
+
export { TODO_WRITE, todoOf, CHECKLIST_WRITE, checklistOf } from "./tools/todo.js";
|
|
34
35
|
export { BUILTIN_PROFILES } from "./agents/builtin.js";
|
|
35
36
|
export { PROFILE_COLORS, createProfile, listProfiles, parseProfile, profileDirs, removeProfile, resolveProfile, type AgentProfile, type ProfileColor, type ProfileListing, type ProfileProblem, type ProfileScope, type ProfileSource, type ProfileVariant } from "./agents/profiles.js";
|
|
36
37
|
export { continuationOf, type Continuation, type ContinuationBlock } from "./sessions/continuation.js";
|
package/dist/sdk/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { hubCatalogSchema, hubEntrySchema, hubOverviewSchema, hubReceiptSchema }
|
|
|
15
15
|
export { Bus, log } from "./core/bus.js";
|
|
16
16
|
export { EMPTY_DIRECTORY } from "./core/env.js";
|
|
17
17
|
export { HarnessError, invalid, notFound, toErrorShape } from "./core/errors.js";
|
|
18
|
+
export { linkOpenCommand, linkTarget, openableTarget, openLink, openLinkTarget, openWithSystem } from "./core/links.js";
|
|
18
19
|
export { findBinary, searchPath } from "./core/path.js";
|
|
19
20
|
export { fileSecretStore, memorySecretStore, secretKey } from "./accounts/secrets.js";
|
|
20
21
|
export { dispatch as harnessCall } from "./tools/dispatch.js";
|
|
@@ -25,7 +26,7 @@ export { STANDARD_CONNECTIONS } from "./prompt/standard.js";
|
|
|
25
26
|
export { SideQuestions, SIDE_QUESTION_INSTRUCTION, buildSideQuestionText } from "./sessions/sideQuestions.js";
|
|
26
27
|
export { describeTool, HARNESS_TOOLS, DEFAULT_TOOL_NAMES, WIKI_WRITE } from "./tools/definitions.js";
|
|
27
28
|
export { SPAWN_SUBAGENT, SUBAGENT_RELATION } from "./tools/spawnSubagent.js";
|
|
28
|
-
export { CHECKLIST_WRITE, checklistOf } from "./tools/
|
|
29
|
+
export { TODO_WRITE, todoOf, CHECKLIST_WRITE, checklistOf } from "./tools/todo.js";
|
|
29
30
|
export { BUILTIN_PROFILES } from "./agents/builtin.js";
|
|
30
31
|
export { PROFILE_COLORS, createProfile, listProfiles, parseProfile, profileDirs, removeProfile, resolveProfile } from "./agents/profiles.js";
|
|
31
32
|
export { continuationOf } from "./sessions/continuation.js";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Built-in feature modules register their contributions here; this registry never loads Pi code.
|
|
2
2
|
import { CONNECTION_TOOLS, NATIVE_GROUP } from "../prompt/compile.js";
|
|
3
3
|
import { ASK_USER, askUserTool } from "../tools/askUser.js";
|
|
4
|
-
import {
|
|
4
|
+
import { TODO_WRITE, todoTool } from "../tools/todo.js";
|
|
5
5
|
import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
|
|
6
6
|
import { sliceOf } from "../sessions/workSlice.js";
|
|
7
7
|
import { SPAWN_SUBAGENT, WAIT_SUBAGENTS } from "../sessions/subagents.js";
|
|
8
8
|
import { subagentTools } from "../tools/spawnSubagent.js";
|
|
9
9
|
import { SESSION_NAME } from "../tools/sessionName.js";
|
|
10
|
-
import { RECALL_TOOLS, SKILL_TOOLS, TRAJECTORY_TOOLS, WIKI_READ_TOOLS, WIKI_WRITE_TOOLS } from "../tools/definitions.js";
|
|
10
|
+
import { PLUGIN_PROPOSAL_TOOLS, RECALL_TOOLS, SKILL_TOOLS, TRAJECTORY_TOOLS, WIKI_READ_TOOLS, WIKI_WRITE_TOOLS } from "../tools/definitions.js";
|
|
11
11
|
import { FIRST_PARTY_PLUGINS } from "./firstParty.js";
|
|
12
12
|
import { WEB_PLUGIN } from "../web/tools.js";
|
|
13
13
|
import { KEYLESS_SEARCH, webPart } from "../web/part.js";
|
|
@@ -79,7 +79,7 @@ const WIKI_PLUGIN = {
|
|
|
79
79
|
};
|
|
80
80
|
const HISTORY_PLUGIN = { id: 'history', title: 'History', description: 'Search earlier conversations and inspect recorded run trajectories.', required: 'Earlier runs are how an agent picks up where work stopped.', tools: tools([...RECALL_TOOLS, ...TRAJECTORY_TOOLS]), toolGroups: ['History'] };
|
|
81
81
|
const ASK_USER_PLUGIN = { id: 'ask-user', title: 'Ask the user', description: 'Lets the agent stop and ask a question with choices.', required: 'An agent must be able to stop and ask you.', tools: [{ name: ASK_USER }] };
|
|
82
|
-
const
|
|
82
|
+
const TODO_PLUGIN = { id: 'todo', title: 'Todo', description: 'Lets the agent keep a todo list of the steps of its work, recorded with the run so progress can be shown and picked up again.', required: 'Progress of longer work is kept through it.', tools: [{ name: TODO_WRITE, description: 'Write the todo list' }] };
|
|
83
83
|
const CHECKPOINT_PLUGIN = { id: 'checkpoint', title: 'Checkpoint', description: 'Lets a bounded reply record a handoff at a work-slice boundary so the next slice starts from it.', required: 'A reply that stops at its budget leaves the state the next one resumes from.', tools: [{ name: CHECKPOINT_WRITE, description: 'Write the handoff checkpoint' }] };
|
|
84
84
|
const CONNECTED_APPS_PLUGIN = {
|
|
85
85
|
id: 'connected-apps',
|
|
@@ -93,6 +93,7 @@ const CONNECTED_APPS_PLUGIN = {
|
|
|
93
93
|
const DOCTOR_PLUGIN = { id: 'doctor', title: 'Doctor', description: 'Local readiness checks and explicitly confirmed live probes for accounts, models and tools.', required: 'Every local terminal has readiness diagnostics.' };
|
|
94
94
|
const WORKTREES_PLUGIN = { id: 'worktrees', title: 'Worktrees', description: 'Create and manage isolated git worktrees, move changes between them, and restore removed worktrees.', required: 'Managed worktrees are available to every local terminal.' };
|
|
95
95
|
const HUB_PLUGIN = { id: 'hub', title: 'Hub', description: 'Browse and install reviewed skills, plugins and documents from configured registries.', required: 'Reviewed Harness resources are available to every host.' };
|
|
96
|
+
const PLUGIN_AUTHORING = { id: 'plugin-authoring', title: 'Plugin authoring', description: 'Propose plugins for a person to review.', required: 'Plugin proposals are reviewed before installation.', tools: tools(PLUGIN_PROPOSAL_TOOLS) };
|
|
96
97
|
const REFINEMENT_PLUGIN = { id: 'refinement', title: 'Refinement', description: 'Improve variants from recorded evidence through isolated refinement runs and reviewed candidates.', required: 'Variant refinement is available to every host.' };
|
|
97
98
|
const CAPABILITIES_PLUGIN = { id: 'capabilities', title: 'Capability discovery', description: 'Inspect the callable tools of the current run, including their input schemas. Discovery never grants access.', required: 'Every interactive session can inspect its own registered capabilities.', tools: [{ name: GET_CAPABILITIES, description: 'Inspect current capabilities' }] };
|
|
98
99
|
const USAGE_PLUGIN = {
|
|
@@ -130,7 +131,7 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
130
131
|
serverMethods: historyServerMethods
|
|
131
132
|
},
|
|
132
133
|
{ descriptor: ASK_USER_PLUGIN, toolOrder: 40, tools: context => context.harnessTools && context.allowedTools.includes(ASK_USER) ? [askUserTool(context.run)] : [] },
|
|
133
|
-
{ descriptor:
|
|
134
|
+
{ descriptor: TODO_PLUGIN, toolOrder: 50, tools: context => context.harnessTools ? [todoTool(context.run)] : [] },
|
|
134
135
|
{ descriptor: CHECKPOINT_PLUGIN, toolOrder: 51, tools: context => context.harnessTools && context.allowedTools.includes(CHECKPOINT_WRITE) ? [checkpointTool(context.run, () => sliceOf(context.env.root, context.runId))] : [] },
|
|
135
136
|
{
|
|
136
137
|
descriptor: CONNECTED_APPS_PLUGIN,
|
|
@@ -139,6 +140,7 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
139
140
|
serverMethods: connectedAppServerMethods
|
|
140
141
|
},
|
|
141
142
|
{ descriptor: HUB_PLUGIN, serverMethods: hubServerMethods },
|
|
143
|
+
{ descriptor: PLUGIN_AUTHORING, ...toolContribution(PLUGIN_PROPOSAL_TOOLS, 31) },
|
|
142
144
|
{ descriptor: REFINEMENT_PLUGIN, serverMethods: refinementServerMethods },
|
|
143
145
|
{ descriptor: SIDE_QUESTIONS_PLUGIN, serverMethods: sideQuestionServerMethods },
|
|
144
146
|
{
|
|
@@ -154,12 +156,14 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
154
156
|
{
|
|
155
157
|
descriptor: ANNOTATIONS_PLUGIN,
|
|
156
158
|
commandPrefix: 'annotate',
|
|
157
|
-
defaultCommand: '
|
|
159
|
+
defaultCommand: 'open',
|
|
158
160
|
commands: [
|
|
159
|
-
{ name: '
|
|
160
|
-
{ name: '
|
|
161
|
+
{ name: 'open', rootOnly: true, description: 'Annotate a page or app and send it to the agent: /annotate: [url | --app <port> | --bridge]' },
|
|
162
|
+
{ name: 'notes', description: 'Every saved web annotation, searchable' },
|
|
163
|
+
{ name: 'submit', description: 'Send this session\'s unsent annotations with your request: /annotate:submit <request>' },
|
|
164
|
+
{ name: 'clear', description: 'Resolve this session\'s unsent annotations and stop sharing them' }
|
|
161
165
|
],
|
|
162
|
-
createWorldPart: ({ options }) => annotationsPart({ root: options.root
|
|
166
|
+
createWorldPart: ({ options }) => annotationsPart({ root: options.root }),
|
|
163
167
|
serverMethods: annotationServerMethods
|
|
164
168
|
},
|
|
165
169
|
{ descriptor: CAPABILITIES_PLUGIN },
|
|
@@ -169,7 +173,8 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
169
173
|
defaultCommand: 'analysis',
|
|
170
174
|
commands: [
|
|
171
175
|
{ name: 'analysis', description: 'Token, cost and latency totals: /usage:analysis [today | 7d | 30d | all]' },
|
|
172
|
-
{ name: 'limit', description: 'Provider plan windows, balances and rate limits: /usage:limit' }
|
|
176
|
+
{ name: 'limit', description: 'Provider plan windows, balances and rate limits: /usage:limit' },
|
|
177
|
+
{ name: 'accounts', description: 'Provider limits across all accounts: /usage:accounts' }
|
|
173
178
|
],
|
|
174
179
|
serverMethods: usageServerMethods
|
|
175
180
|
},
|
|
@@ -14,6 +14,8 @@ export interface BuiltinPluginCommand {
|
|
|
14
14
|
name: string;
|
|
15
15
|
description: string;
|
|
16
16
|
legacyBare: boolean;
|
|
17
|
+
/** Reached only as the namespace root (`/annotate:`); it has no `/<prefix>:<name>` route and is not listed by name. */
|
|
18
|
+
rootOnly?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export interface BuiltinWorldContext {
|
|
19
21
|
options: WorldOptions;
|
|
@@ -36,7 +38,7 @@ export interface BuiltinPluginModule {
|
|
|
36
38
|
descriptor: PluginDescriptor;
|
|
37
39
|
/** Stable terminal command namespace; defaults to the plugin id. */
|
|
38
40
|
commandPrefix?: string;
|
|
39
|
-
/** Command invoked by the namespace root, such as `/annotate:`. */
|
|
41
|
+
/** Command invoked by the namespace root, such as `/annotate:`. Mark it `rootOnly` to give it no named route. */
|
|
40
42
|
defaultCommand?: string;
|
|
41
43
|
/** Stable order for model tools, independent of descriptor display order. */
|
|
42
44
|
toolOrder?: number;
|
|
@@ -27,6 +27,10 @@ export function createBuiltinPluginRegistry(modules) {
|
|
|
27
27
|
if (module.defaultCommand && !module.commands?.some(command => command.name === module.defaultCommand)) {
|
|
28
28
|
throw new Error(`Unknown default command for built-in plugin ${id}: ${module.defaultCommand}`);
|
|
29
29
|
}
|
|
30
|
+
for (const command of module.commands ?? []) {
|
|
31
|
+
if (command.rootOnly && command.name !== module.defaultCommand)
|
|
32
|
+
throw new Error(`Root-only command ${prefix}:${command.name} must be the default command of built-in plugin ${id}.`);
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
const commands = modules.flatMap(module => (module.commands ?? []).map(command => ({
|
|
32
36
|
pluginId: module.descriptor.id,
|
|
@@ -34,12 +38,14 @@ export function createBuiltinPluginRegistry(modules) {
|
|
|
34
38
|
...command,
|
|
35
39
|
legacyBare: !!module.descriptor.required
|
|
36
40
|
})));
|
|
37
|
-
const
|
|
41
|
+
const allRoutes = new Map(commands.map(command => [`${command.prefix}:${command.name}`, command]));
|
|
42
|
+
const routed = commands.filter(command => !command.rootOnly);
|
|
43
|
+
const byRoute = new Map(routed.map(command => [`${command.prefix}:${command.name}`, command]));
|
|
38
44
|
const byName = new Map();
|
|
39
|
-
for (const command of
|
|
45
|
+
for (const command of routed)
|
|
40
46
|
byName.set(command.name, [...(byName.get(command.name) ?? []), command]);
|
|
41
47
|
const defaults = new Map(modules.flatMap(module => module.defaultCommand
|
|
42
|
-
? [[module.commandPrefix ?? module.descriptor.id,
|
|
48
|
+
? [[module.commandPrefix ?? module.descriptor.id, allRoutes.get(`${module.commandPrefix ?? module.descriptor.id}:${module.defaultCommand}`)]]
|
|
43
49
|
: []));
|
|
44
50
|
const toolModules = modules.map((module, index) => ({ module, index })).filter(({ module }) => module.tools)
|
|
45
51
|
.sort((left, right) => (left.module.toolOrder ?? left.index) - (right.module.toolOrder ?? right.index));
|