@xingular/harness 1.1.1 → 1.3.1-unstable.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +36 -28
- package/dist/cli.js +10837 -6159
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.js +18 -4
- package/dist/sdk/agents/planResearch.d.ts +27 -0
- package/dist/sdk/agents/planResearch.js +82 -0
- package/dist/sdk/agents/profiles.d.ts +5 -1
- package/dist/sdk/agents/profiles.js +21 -5
- package/dist/sdk/annotations/pageScript.js +349 -48
- package/dist/sdk/annotations/part.js +34 -23
- package/dist/sdk/annotations/store.d.ts +7 -0
- package/dist/sdk/annotations/store.js +13 -5
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/cli.js +5 -4
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +258 -0
- package/dist/sdk/commands/registry.js +48 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +20 -0
- package/dist/sdk/core/workspaceBashTool.js +50 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
- package/dist/sdk/core/workspaceSandbox.js +360 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
- package/dist/sdk/core/workspaceWriteOperations.js +224 -0
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +7 -0
- package/dist/sdk/harness.js +42 -2
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +8 -3
- package/dist/sdk/index.js +4 -1
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +32 -15
- package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
- package/dist/sdk/plugins/builtinRegistry.js +30 -8
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +6 -5
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +14 -0
- package/dist/sdk/prompt/compile.js +65 -31
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +22 -2
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +7 -2
- package/dist/sdk/sessions/continuation.js +11 -6
- package/dist/sdk/sessions/contracts.d.ts +190 -4
- package/dist/sdk/sessions/contracts.js +71 -8
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +14 -3
- package/dist/sdk/sessions/planMode.js +74 -17
- package/dist/sdk/sessions/registry.d.ts +1 -1
- package/dist/sdk/sessions/registry.js +2 -2
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +38 -1
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +161 -13
- package/dist/sdk/sessions/sessions.d.ts +23 -3
- package/dist/sdk/sessions/sessions.js +225 -22
- package/dist/sdk/sessions/subagentService.d.ts +24 -0
- package/dist/sdk/sessions/subagentService.js +225 -26
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +106 -0
- package/dist/sdk/sessions/workSlice.js +329 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browserControl.js +2 -1
- package/dist/sdk/terminal/controller.d.ts +31 -3
- package/dist/sdk/terminal/controller.js +81 -29
- 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 +43 -0
- package/dist/sdk/terminal/opentui/theme.js +88 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +177 -63
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
- package/dist/sdk/terminal/subagentMonitor.js +61 -9
- package/dist/sdk/terminal/terminals.d.ts +26 -2
- package/dist/sdk/terminal/terminals.js +325 -43
- 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 +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- 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 +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +17 -4
- package/dist/sdk/terminal/tui/commands.d.ts +6 -0
- package/dist/sdk/terminal/tui/commands.js +213 -124
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/pageOverlay.js +21 -5
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- 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/notesPage.d.ts +5 -5
- package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -6
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
- package/dist/sdk/terminal/tui/runtime.js +23 -3
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +11 -8
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/terminal-opentui.js +9204 -4812
- package/dist/terminal-pi.js +1848 -272
- 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 +169 -36
- package/docs/architecture/04-prompt.md +55 -15
- package/docs/architecture/06-plugins-and-capabilities.md +36 -15
- package/docs/architecture/07-tools-and-gate.md +131 -25
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +39 -3
- package/docs/architecture/10-server-protocol.md +71 -9
- package/docs/architecture/11-terminal.md +148 -60
- package/docs/architecture/12-storage.md +30 -2
- package/docs/architecture/13-world-tools.md +22 -1
- package/docs/architecture/14-surfaces-and-parity.md +58 -8
- package/docs/architecture/README.md +20 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +258 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- 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/README.md +24 -6
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- 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/doctor.md +14 -1
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +83 -27
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/4.0.0.md +10 -9
- package/docs/releases/README.md +35 -30
- package/docs/study/00-index.md +3 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/terminal-tui.md +162 -82
- package/package.json +4 -3
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -54
|
@@ -3,8 +3,8 @@ import { type Bus } from "../core/bus.js";
|
|
|
3
3
|
import type { RunEvent, RunRecord } from "../sessions/contracts.js";
|
|
4
4
|
import * as choices from "./choices.js";
|
|
5
5
|
import * as env from "./env.js";
|
|
6
|
-
import { type SecretStore } from "./secrets.js";
|
|
7
|
-
import type { Account, AccountsOverview } from "./store.js";
|
|
6
|
+
import { type CredentialLockOptions, type SecretStore } from "./secrets.js";
|
|
7
|
+
import type { Account, AccountsOverview, Limit } from "./store.js";
|
|
8
8
|
export type IssueCategory = 'auth_expired' | 'rate_limited' | 'quota' | 'network' | 'unknown';
|
|
9
9
|
/** Best-effort classification of a provider error message. */
|
|
10
10
|
export declare function classifyMessage(message: string): IssueCategory | null;
|
|
@@ -35,11 +35,13 @@ export interface AccountsOptions {
|
|
|
35
35
|
dir: string;
|
|
36
36
|
secrets: SecretStore;
|
|
37
37
|
bus: Bus;
|
|
38
|
+
credentialLock?: CredentialLockOptions;
|
|
38
39
|
}
|
|
39
40
|
export declare class Accounts {
|
|
40
41
|
readonly dir: string;
|
|
41
42
|
private readonly secrets;
|
|
42
43
|
private readonly bus;
|
|
44
|
+
private readonly credentialLockOptions;
|
|
43
45
|
private readonly runtimes;
|
|
44
46
|
private readonly logins;
|
|
45
47
|
private readonly latestLogins;
|
|
@@ -48,6 +50,11 @@ export declare class Accounts {
|
|
|
48
50
|
/** The credentials this process wrote to, or found in, each account's auth.json when its first terminal began. */
|
|
49
51
|
private readonly terminalSnapshots;
|
|
50
52
|
private installationDeviceIdPromise;
|
|
53
|
+
/**
|
|
54
|
+
* Lets the limits service observe each account runtime's provider responses. The Harness sets
|
|
55
|
+
* it once; with none, a runtime behaves exactly as before.
|
|
56
|
+
*/
|
|
57
|
+
observeModels: ((models: unknown, accountId: string) => void) | null;
|
|
51
58
|
constructor(options: AccountsOptions);
|
|
52
59
|
private changed;
|
|
53
60
|
overview(): AccountsOverview;
|
|
@@ -60,13 +67,17 @@ export declare class Accounts {
|
|
|
60
67
|
resolve(accountId?: string | null, poolId?: string | null): Account;
|
|
61
68
|
context(account: Account): env.AccountContext;
|
|
62
69
|
readCredential(accountId: string, provider: string): Promise<Credential | null>;
|
|
70
|
+
private credentialLock;
|
|
63
71
|
/** Stores a provider's credential, or removes it when `credential` is `null`. */
|
|
64
72
|
writeCredential(accountId: string, provider: string, credential: Credential | null): Promise<void>;
|
|
73
|
+
/** The entire refresh read/modify/write is coordinated across services and processes. */
|
|
74
|
+
modifyCredential(accountId: string, provider: string, change: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
|
|
75
|
+
private persistCredential;
|
|
65
76
|
listCredentials(accountId: string): Promise<{
|
|
66
77
|
providerId: string;
|
|
67
78
|
type: 'api_key' | 'oauth';
|
|
68
79
|
}[]>;
|
|
69
|
-
/** Pi
|
|
80
|
+
/** Pi and explicit account edits share the same cross-process credential lock. */
|
|
70
81
|
private credentialStore;
|
|
71
82
|
/** Pi's OAuth device ID belongs to this Harness installation, not to one isolated account. */
|
|
72
83
|
installationDeviceId(): Promise<string>;
|
|
@@ -160,6 +171,19 @@ export declare class Accounts {
|
|
|
160
171
|
private releaseLease;
|
|
161
172
|
/** Tracks account health and limits from run events. `run` is the event's run record, when it is known. */
|
|
162
173
|
onRunEvent(event: RunEvent, run: RunRecord | null): void;
|
|
174
|
+
/**
|
|
175
|
+
* Records what a provider reported about its limits. An endpoint reading replaces that provider's
|
|
176
|
+
* limits; a header reading merges by id. Windows whose reset has passed are dropped. No credential
|
|
177
|
+
* is stored here.
|
|
178
|
+
*/
|
|
179
|
+
applyLimits(accountId: string, readouts: readonly {
|
|
180
|
+
provider: string;
|
|
181
|
+
limits: Limit[];
|
|
182
|
+
source: 'headers' | 'provider' | 'estimate' | 'unavailable';
|
|
183
|
+
reason: string | null;
|
|
184
|
+
observedAt: string;
|
|
185
|
+
plan?: string | null;
|
|
186
|
+
}[]): AccountsOverview;
|
|
163
187
|
private applyStatus;
|
|
164
188
|
}
|
|
165
189
|
export {};
|
|
@@ -10,7 +10,7 @@ import { atomicWrite, now, readJson, safeId } from "../core/fs.js";
|
|
|
10
10
|
import { loadPi } from "../pi/runtime.js";
|
|
11
11
|
import * as choices from "./choices.js";
|
|
12
12
|
import * as env from "./env.js";
|
|
13
|
-
import { secretKey } from "./secrets.js";
|
|
13
|
+
import { secretKey, withCredentialLock } from "./secrets.js";
|
|
14
14
|
import * as store from "./store.js";
|
|
15
15
|
/** Best-effort classification of a provider error message. */
|
|
16
16
|
export function classifyMessage(message) {
|
|
@@ -64,6 +64,7 @@ export class Accounts {
|
|
|
64
64
|
dir;
|
|
65
65
|
secrets;
|
|
66
66
|
bus;
|
|
67
|
+
credentialLockOptions;
|
|
67
68
|
runtimes = new Map();
|
|
68
69
|
logins = new Map();
|
|
69
70
|
latestLogins = new Map();
|
|
@@ -72,10 +73,16 @@ export class Accounts {
|
|
|
72
73
|
/** The credentials this process wrote to, or found in, each account's auth.json when its first terminal began. */
|
|
73
74
|
terminalSnapshots = new Map();
|
|
74
75
|
installationDeviceIdPromise = null;
|
|
76
|
+
/**
|
|
77
|
+
* Lets the limits service observe each account runtime's provider responses. The Harness sets
|
|
78
|
+
* it once; with none, a runtime behaves exactly as before.
|
|
79
|
+
*/
|
|
80
|
+
observeModels = null;
|
|
75
81
|
constructor(options) {
|
|
76
82
|
this.dir = options.dir;
|
|
77
83
|
this.secrets = options.secrets;
|
|
78
84
|
this.bus = options.bus;
|
|
85
|
+
this.credentialLockOptions = options.credentialLock ?? {};
|
|
79
86
|
}
|
|
80
87
|
changed(payload = {}) {
|
|
81
88
|
this.bus.emit({ type: 'accounts.changed', ...payload });
|
|
@@ -116,18 +123,40 @@ export class Accounts {
|
|
|
116
123
|
return null;
|
|
117
124
|
}
|
|
118
125
|
}
|
|
126
|
+
credentialLock(key, work) {
|
|
127
|
+
return withCredentialLock(join(this.dir, '.credential-locks'), key, work, this.credentialLockOptions);
|
|
128
|
+
}
|
|
119
129
|
/** Stores a provider's credential, or removes it when `credential` is `null`. */
|
|
120
130
|
async writeCredential(accountId, provider, credential) {
|
|
121
131
|
this.find(accountId);
|
|
122
132
|
const name = validProvider(provider);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
await this.credentialLock(`provider/${accountId}/${name}`, () => this.persistCredential(accountId, name, credential));
|
|
134
|
+
}
|
|
135
|
+
/** The entire refresh read/modify/write is coordinated across services and processes. */
|
|
136
|
+
async modifyCredential(accountId, provider, change) {
|
|
137
|
+
this.find(accountId);
|
|
138
|
+
const name = validProvider(provider);
|
|
139
|
+
return this.credentialLock(`provider/${accountId}/${name}`, async () => {
|
|
140
|
+
const current = (await this.readCredential(accountId, name)) ?? undefined;
|
|
141
|
+
const updated = await change(current);
|
|
142
|
+
if (updated !== undefined)
|
|
143
|
+
await this.persistCredential(accountId, name, updated);
|
|
144
|
+
return updated ?? current;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async persistCredential(accountId, name, credential) {
|
|
148
|
+
// Different providers still share the account's provider index. Keep its short commit serialized.
|
|
149
|
+
await this.credentialLock(`account/${accountId}`, async () => {
|
|
150
|
+
this.find(accountId);
|
|
151
|
+
if (credential === null)
|
|
152
|
+
await this.secrets.delete(secretKey(accountId, `pi-auth/${name}`));
|
|
153
|
+
else
|
|
154
|
+
await this.secrets.set(secretKey(accountId, `pi-auth/${name}`), JSON.stringify(credential));
|
|
155
|
+
store.updateState(this.dir, accountId, (state) => {
|
|
156
|
+
state.piProviders = state.piProviders.filter((existing) => existing !== name);
|
|
157
|
+
if (credential !== null)
|
|
158
|
+
state.piProviders = [...state.piProviders, name].sort();
|
|
159
|
+
});
|
|
131
160
|
});
|
|
132
161
|
}
|
|
133
162
|
async listCredentials(accountId) {
|
|
@@ -140,25 +169,13 @@ export class Accounts {
|
|
|
140
169
|
}
|
|
141
170
|
return listed;
|
|
142
171
|
}
|
|
143
|
-
/** Pi
|
|
172
|
+
/** Pi and explicit account edits share the same cross-process credential lock. */
|
|
144
173
|
credentialStore(accountId) {
|
|
145
|
-
let chain = Promise.resolve();
|
|
146
|
-
const serialize = (work) => {
|
|
147
|
-
const next = chain.then(work);
|
|
148
|
-
chain = next.catch(() => undefined);
|
|
149
|
-
return next;
|
|
150
|
-
};
|
|
151
174
|
return {
|
|
152
175
|
read: async (providerId) => (await this.readCredential(accountId, providerId)) ?? undefined,
|
|
153
176
|
list: () => this.listCredentials(accountId),
|
|
154
|
-
modify: (providerId, change) =>
|
|
155
|
-
|
|
156
|
-
const updated = await change(current);
|
|
157
|
-
if (updated !== undefined)
|
|
158
|
-
await this.writeCredential(accountId, providerId, updated);
|
|
159
|
-
return updated ?? current;
|
|
160
|
-
}),
|
|
161
|
-
delete: (providerId) => serialize(() => this.writeCredential(accountId, providerId, null))
|
|
177
|
+
modify: (providerId, change) => this.modifyCredential(accountId, providerId, change),
|
|
178
|
+
delete: (providerId) => this.writeCredential(accountId, providerId, null)
|
|
162
179
|
};
|
|
163
180
|
}
|
|
164
181
|
/** Pi's OAuth device ID belongs to this Harness installation, not to one isolated account. */
|
|
@@ -187,6 +204,14 @@ export class Accounts {
|
|
|
187
204
|
const configDir = 'configDir' in account ? account.configDir : env.ensureHome(this.dir, account);
|
|
188
205
|
runtime = loadPi()
|
|
189
206
|
.then(({ ModelRuntime }) => ModelRuntime.create({ credentials: this.credentialStore(id), modelsPath: join(configDir, 'models.json') }))
|
|
207
|
+
.then((created) => {
|
|
208
|
+
// Reading provider responses never changes what a request does; a broken observer is ignored.
|
|
209
|
+
try {
|
|
210
|
+
this.observeModels?.(created, id);
|
|
211
|
+
}
|
|
212
|
+
catch { /* observation is best effort */ }
|
|
213
|
+
return created;
|
|
214
|
+
})
|
|
190
215
|
.catch((error) => {
|
|
191
216
|
this.runtimes.delete(id);
|
|
192
217
|
throw error;
|
|
@@ -690,6 +715,35 @@ export class Accounts {
|
|
|
690
715
|
this.changed({ accountId, health: state.health, previous });
|
|
691
716
|
}
|
|
692
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Records what a provider reported about its limits. An endpoint reading replaces that provider's
|
|
720
|
+
* limits; a header reading merges by id. Windows whose reset has passed are dropped. No credential
|
|
721
|
+
* is stored here.
|
|
722
|
+
*/
|
|
723
|
+
applyLimits(accountId, readouts) {
|
|
724
|
+
this.find(accountId);
|
|
725
|
+
const expired = (limit) => {
|
|
726
|
+
const time = limit.resetsAt ? Date.parse(limit.resetsAt) : Number.NaN;
|
|
727
|
+
return !Number.isNaN(time) && time <= Date.now();
|
|
728
|
+
};
|
|
729
|
+
store.updateState(this.dir, accountId, (state) => {
|
|
730
|
+
for (const readout of readouts) {
|
|
731
|
+
// An endpoint read is the provider's whole answer; a header read only adds what it saw.
|
|
732
|
+
const kept = new Map((readout.source === 'provider' ? [] : state.limits.filter((limit) => limit.provider === readout.provider && !expired(limit)))
|
|
733
|
+
.map((limit) => [limit.id, limit]));
|
|
734
|
+
for (const limit of readout.limits)
|
|
735
|
+
kept.set(limit.id, limit);
|
|
736
|
+
state.limits = [...state.limits.filter((limit) => limit.provider !== readout.provider), ...kept.values()];
|
|
737
|
+
const entry = { provider: readout.provider, source: readout.source, reason: readout.reason, observedAt: readout.observedAt };
|
|
738
|
+
state.providerLimits = [...state.providerLimits.filter((existing) => existing.provider !== readout.provider), entry];
|
|
739
|
+
if (readout.plan)
|
|
740
|
+
state.plan = readout.plan;
|
|
741
|
+
}
|
|
742
|
+
state.limits.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
743
|
+
});
|
|
744
|
+
this.changed({ accountId });
|
|
745
|
+
return this.overview();
|
|
746
|
+
}
|
|
693
747
|
applyStatus(event) {
|
|
694
748
|
const payload = event.payload;
|
|
695
749
|
if (typeof payload.accountId !== 'string')
|
|
@@ -3,6 +3,18 @@ export interface SecretStore {
|
|
|
3
3
|
set(key: string, value: string): Promise<void>;
|
|
4
4
|
delete(key: string): Promise<void>;
|
|
5
5
|
}
|
|
6
|
+
export interface CredentialLockOptions {
|
|
7
|
+
/** Acquisition only; an acquired callback is never unlocked while still running. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
pollMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Cross-process Lamport bakery lock. Unique participant directories make dead-pid reclamation
|
|
13
|
+
* safe: a stale cleanup can never unlink a successor's lock. No age-based stealing of live refreshes.
|
|
14
|
+
* The directory contains only hashed keys, pids, random IDs and tickets, never credential contents.
|
|
15
|
+
* All processes sharing a backend must use the same coordination directory on a local filesystem.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withCredentialLock<T>(dir: string, key: string, work: () => Promise<T>, options?: CredentialLockOptions): Promise<T>;
|
|
6
18
|
/** `<account id>/<secret name>`: the key an account's secret is stored under. */
|
|
7
19
|
export declare const secretKey: (accountId: string, name: string) => string;
|
|
8
20
|
/** Secrets held only for the life of the process. For tests and throwaway runs. */
|
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
// Where credentials live. The harness never writes a secret to its own files: it asks the
|
|
2
2
|
// store the embedding application provides (a Keychain, an encrypted vault), by key.
|
|
3
|
-
import { chmodSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { chmodSync, closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
import { readJsonOr } from "../core/fs.js";
|
|
8
|
+
/** Exclusive temporary files are private before the first byte is written, not just after rename. */
|
|
9
|
+
function privateWrite(path, value) {
|
|
10
|
+
const temporary = join(dirname(path), `.credential-${randomUUID()}.tmp`);
|
|
11
|
+
try {
|
|
12
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(fd, value);
|
|
15
|
+
fsyncSync(fd);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
closeSync(fd);
|
|
19
|
+
}
|
|
20
|
+
renameSync(temporary, path);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
rmSync(temporary, { force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Cross-process Lamport bakery lock. Unique participant directories make dead-pid reclamation
|
|
28
|
+
* safe: a stale cleanup can never unlink a successor's lock. No age-based stealing of live refreshes.
|
|
29
|
+
* The directory contains only hashed keys, pids, random IDs and tickets, never credential contents.
|
|
30
|
+
* All processes sharing a backend must use the same coordination directory on a local filesystem.
|
|
31
|
+
*/
|
|
32
|
+
export async function withCredentialLock(dir, key, work, options = {}) {
|
|
33
|
+
const timeout = options.timeoutMs ?? 30_000;
|
|
34
|
+
const poll = options.pollMs ?? 20;
|
|
35
|
+
if (!Number.isFinite(timeout) || timeout < 0 || timeout > 300_000 || !Number.isFinite(poll) || poll < 1 || poll > 1000) {
|
|
36
|
+
throw new Error('Invalid credential lock wait bounds.');
|
|
37
|
+
}
|
|
38
|
+
const started = performance.now();
|
|
39
|
+
const folder = join(dir, createHash('sha256').update(key).digest('hex'));
|
|
40
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
41
|
+
if (!lstatSync(dir).isDirectory() || !lstatSync(folder).isDirectory())
|
|
42
|
+
throw new Error('Invalid credential lock directory.');
|
|
43
|
+
chmodSync(dir, 0o700);
|
|
44
|
+
chmodSync(folder, 0o700);
|
|
45
|
+
const id = `${process.pid}-${randomUUID()}`;
|
|
46
|
+
const own = join(folder, id);
|
|
47
|
+
mkdirSync(own, { mode: 0o700 });
|
|
48
|
+
const participants = () => {
|
|
49
|
+
const entries = [];
|
|
50
|
+
for (const name of readdirSync(folder)) {
|
|
51
|
+
if (!/^\d+-[0-9a-f-]{36}$/.test(name))
|
|
52
|
+
throw new Error('Invalid credential lock metadata.');
|
|
53
|
+
const pid = Number(name.slice(0, name.indexOf('-')));
|
|
54
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
55
|
+
throw new Error('Invalid credential lock metadata.');
|
|
56
|
+
let alive = true;
|
|
57
|
+
try {
|
|
58
|
+
process.kill(pid, 0);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === 'ESRCH')
|
|
62
|
+
alive = false;
|
|
63
|
+
}
|
|
64
|
+
const participant = join(folder, name);
|
|
65
|
+
if (!alive) {
|
|
66
|
+
rmSync(participant, { recursive: true, force: true });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
// The absent ticket is the bakery algorithm's choosing flag.
|
|
71
|
+
const text = readFileSync(join(participant, 'ticket'), 'utf8');
|
|
72
|
+
const ticket = Number(text);
|
|
73
|
+
if (!Number.isSafeInteger(ticket) || ticket <= 0)
|
|
74
|
+
throw new Error('Invalid credential lock metadata.');
|
|
75
|
+
entries.push({ id: name, ticket });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT')
|
|
79
|
+
throw error;
|
|
80
|
+
if (lstatExists(participant))
|
|
81
|
+
entries.push({ id: name, ticket: null });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
const ticket = Math.max(0, ...participants().map(entry => entry.ticket ?? 0)) + 1;
|
|
88
|
+
privateWrite(join(own, 'ticket'), String(ticket));
|
|
89
|
+
for (;;) {
|
|
90
|
+
const blocked = participants().some(entry => entry.id !== id && (entry.ticket === null || entry.ticket < ticket || (entry.ticket === ticket && entry.id < id)));
|
|
91
|
+
if (!blocked)
|
|
92
|
+
return await work();
|
|
93
|
+
if (performance.now() - started >= timeout)
|
|
94
|
+
throw new Error('Timed out waiting for credential modification; no credentials were changed.');
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(poll, Math.max(1, timeout - (performance.now() - started)))));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
rmSync(own, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function lstatExists(path) {
|
|
103
|
+
try {
|
|
104
|
+
lstatSync(path);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code === 'ENOENT')
|
|
109
|
+
return false;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
6
113
|
/** `<account id>/<secret name>`: the key an account's secret is stored under. */
|
|
7
114
|
export const secretKey = (accountId, name) => `${accountId}/${name}`;
|
|
8
115
|
/** Secrets held only for the life of the process. For tests and throwaway runs. */
|
|
@@ -23,18 +130,18 @@ export function fileSecretStore(path) {
|
|
|
23
130
|
const read = () => readJsonOr(path, {});
|
|
24
131
|
const write = (values) => {
|
|
25
132
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
26
|
-
|
|
133
|
+
privateWrite(path, `${JSON.stringify(values, null, 2)}\n`);
|
|
27
134
|
chmodSync(path, 0o600);
|
|
28
135
|
};
|
|
29
136
|
return {
|
|
30
137
|
get: async (key) => read()[key] ?? null,
|
|
31
|
-
set:
|
|
32
|
-
delete: async (
|
|
138
|
+
set: (key, value) => withCredentialLock(`${path}.locks`, 'file', async () => write({ ...read(), [key]: value })),
|
|
139
|
+
delete: (key) => withCredentialLock(`${path}.locks`, 'file', async () => {
|
|
33
140
|
const values = read();
|
|
34
141
|
if (key in values) {
|
|
35
142
|
delete values[key];
|
|
36
143
|
write(values);
|
|
37
144
|
}
|
|
38
|
-
}
|
|
145
|
+
})
|
|
39
146
|
};
|
|
40
147
|
}
|
|
@@ -25,10 +25,33 @@ export interface Limit {
|
|
|
25
25
|
id: string;
|
|
26
26
|
label: string;
|
|
27
27
|
usedPercent?: number | null;
|
|
28
|
+
/** The same window from the other side: quota left. Additive for surfaces that read limits as remaining. */
|
|
29
|
+
remainingPercent?: number | null;
|
|
28
30
|
resetsAt?: string | null;
|
|
29
31
|
windowMinutes?: number | null;
|
|
30
|
-
/** `allowed`, `warning`, or `
|
|
32
|
+
/** `allowed`, `warning`, `rejected`, or `configured` when the provider reports it. */
|
|
31
33
|
status?: string | null;
|
|
34
|
+
/** Which provider the reading belongs to. Older records may omit it. */
|
|
35
|
+
provider?: string | null;
|
|
36
|
+
/** What kind of ceiling this is. */
|
|
37
|
+
kind?: 'window' | 'rate' | 'credit' | 'configured' | 'error';
|
|
38
|
+
/** The unit of `used`, `limit` and `remaining`. */
|
|
39
|
+
unit?: 'percent' | 'requests' | 'tokens' | 'usd' | 'currency' | 'count';
|
|
40
|
+
used?: number | null;
|
|
41
|
+
limit?: number | null;
|
|
42
|
+
remaining?: number | null;
|
|
43
|
+
/** How the reading was obtained. */
|
|
44
|
+
source?: 'headers' | 'provider' | 'estimate' | 'error';
|
|
45
|
+
observedAt?: string | null;
|
|
46
|
+
/** A short provider note, such as a balance currency. */
|
|
47
|
+
detail?: string | null;
|
|
48
|
+
}
|
|
49
|
+
/** The last readout for one provider of one account, including when nothing could be read. */
|
|
50
|
+
export interface ProviderLimitState {
|
|
51
|
+
provider: string;
|
|
52
|
+
source: 'headers' | 'provider' | 'estimate' | 'unavailable';
|
|
53
|
+
reason: string | null;
|
|
54
|
+
observedAt: string;
|
|
32
55
|
}
|
|
33
56
|
export interface Issue {
|
|
34
57
|
/** `auth_expired`, `rate_limited`, `quota`, `network`, or `unknown`. */
|
|
@@ -45,6 +68,8 @@ export interface AccountState {
|
|
|
45
68
|
plan: string | null;
|
|
46
69
|
identity: string | null;
|
|
47
70
|
limits: Limit[];
|
|
71
|
+
/** What the last limit readout found for each provider, including an unavailable one. */
|
|
72
|
+
providerLimits: ProviderLimitState[];
|
|
48
73
|
lastError: Issue | null;
|
|
49
74
|
lastCheckedAt: string | null;
|
|
50
75
|
cliVersion: string | null;
|
|
@@ -18,7 +18,7 @@ import { appendLine, newId, now, readJsonOr, safeId, writeJson } from "../core/f
|
|
|
18
18
|
import { charCount, compareText, lines, takeChars } from "../core/text.js";
|
|
19
19
|
/** The only provider the harness runs. Records of other providers are kept in the file and ignored. */
|
|
20
20
|
export const PROVIDER = 'pi';
|
|
21
|
-
export const emptyState = () => ({ health: 'unknown', signedIn: null, authMethod: null, plan: null, identity: null, limits: [], lastError: null, lastCheckedAt: null, cliVersion: null, capabilities: [], piProviders: [] });
|
|
21
|
+
export const emptyState = () => ({ health: 'unknown', signedIn: null, authMethod: null, plan: null, identity: null, limits: [], providerLimits: [], lastError: null, lastCheckedAt: null, cliVersion: null, capabilities: [], piProviders: [] });
|
|
22
22
|
// ---- Files ------------------------------------------------------------------------------
|
|
23
23
|
const registryPath = (dir) => join(dir, 'accounts.json');
|
|
24
24
|
const statePath = (dir, accountId) => join(dir, safeId(accountId), 'state.json');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { PLAN_RESEARCH_PLUGINS } from "./planResearch.js";
|
|
1
2
|
const RECALL = ['history_search', 'history_read', 'trajectory_search', 'trajectory_read', 'wiki_search', 'wiki_read', 'wiki_list', 'skill_load'];
|
|
2
|
-
const PROGRESS = ['
|
|
3
|
+
const PROGRESS = ['todo_write', 'session_name'];
|
|
3
4
|
const WEB = ['web_search', 'web_fetch'];
|
|
4
5
|
export const BUILTIN_PROFILES = [
|
|
5
6
|
{
|
|
@@ -9,7 +10,7 @@ export const BUILTIN_PROFILES = [
|
|
|
9
10
|
instructions: [
|
|
10
11
|
'You carry out one piece of work from your brief, end to end.',
|
|
11
12
|
'- Read the code you change first and follow the conventions you find.',
|
|
12
|
-
'- Keep a
|
|
13
|
+
'- Keep a todo list with todo_write when the brief has more than one step, and name your session with session_name.',
|
|
13
14
|
'- Make the change, then check it: run the relevant tests, type checks or commands.',
|
|
14
15
|
'- Stay inside the brief. If something outside it blocks you, stop and say so instead of widening the change.',
|
|
15
16
|
'- Finish with a short report: what you changed (files), how you checked it, and anything left open.'
|
|
@@ -24,11 +25,24 @@ export const BUILTIN_PROFILES = [
|
|
|
24
25
|
instructions: [
|
|
25
26
|
'You answer a question about the code, files or history in your brief. You never change anything.',
|
|
26
27
|
'- Search broadly first, then read the parts that matter.',
|
|
27
|
-
'- Keep a
|
|
28
|
+
'- Keep a todo list with todo_write when the search has several parts, and name your session with session_name.',
|
|
28
29
|
'- Cite what you found as path:line.',
|
|
29
30
|
'- Return conclusions, not file dumps: what you found, where, and how sure you are.'
|
|
30
31
|
].join('\n'),
|
|
31
32
|
tools: ['read', 'grep', 'find', 'ls', ...PROGRESS, ...RECALL, ...WEB],
|
|
32
|
-
plugins:
|
|
33
|
+
plugins: [...PLAN_RESEARCH_PLUGINS], variant: null, model: null, effort: null
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'researcher',
|
|
37
|
+
description: 'Researches code, history and public sources without changing anything.',
|
|
38
|
+
color: 'purple',
|
|
39
|
+
instructions: [
|
|
40
|
+
'You investigate the bounded research question in your brief and return concise, evidence-backed findings to the parent.',
|
|
41
|
+
'- Use read-only repository and public-web sources. Never modify files, run commands, call connected apps, or ask the user.',
|
|
42
|
+
'- Prefer primary sources. Cite repository claims as path:line and external claims with source URLs.',
|
|
43
|
+
'- Separate observed facts from inference, state uncertainty, and return only findings relevant to the brief; do not implement.'
|
|
44
|
+
].join('\n'),
|
|
45
|
+
tools: ['read', 'grep', 'find', 'ls', ...PROGRESS, ...RECALL, ...WEB],
|
|
46
|
+
plugins: [...PLAN_RESEARCH_PLUGINS], variant: null, model: null, effort: null
|
|
33
47
|
}
|
|
34
48
|
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CapabilitySet } from "../capabilities/types.js";
|
|
2
|
+
import type { PluginDescriptor } from "../plugins/types.js";
|
|
3
|
+
import type { AgentProfile } from "./profiles.js";
|
|
4
|
+
export declare const PLAN_RESEARCH_TOOLS: ReadonlySet<string>;
|
|
5
|
+
export declare const PLAN_RESEARCH_PLUGINS: readonly string[];
|
|
6
|
+
export declare const PLAN_RESEARCH_EVIDENCE_TOOLS: ReadonlySet<string>;
|
|
7
|
+
/** Capability snapshot needed to tell whether a profile can research in this particular run. */
|
|
8
|
+
export interface PlanResearchContext {
|
|
9
|
+
frozenTools: readonly string[];
|
|
10
|
+
contributions: readonly string[];
|
|
11
|
+
descriptors: readonly PluginDescriptor[];
|
|
12
|
+
}
|
|
13
|
+
export interface PlanResearchEligibility {
|
|
14
|
+
eligible: boolean;
|
|
15
|
+
refusal: string | null;
|
|
16
|
+
/** The exact safe child snapshot to launch when eligible. */
|
|
17
|
+
capabilities: CapabilitySet;
|
|
18
|
+
unavailableTools: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Whether the profile's explicit allowlists are restricted to read-only research. */
|
|
21
|
+
export declare function planResearchProfile(profile: Pick<AgentProfile, 'name' | 'tools' | 'plugins'> | null | undefined): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Evaluate the same effective intersection used for launch, including tool ownership removed by
|
|
24
|
+
* profile plugin narrowing. An explicit request is a strict override; omission inherits the safe
|
|
25
|
+
* profile/parent intersection.
|
|
26
|
+
*/
|
|
27
|
+
export declare function planResearchEligibility(profile: Pick<AgentProfile, 'name' | 'tools' | 'plugins'> | null | undefined, context: PlanResearchContext, requested?: readonly string[]): PlanResearchEligibility;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { narrowForProfile } from "./narrow.js";
|
|
2
|
+
export const PLAN_RESEARCH_TOOLS = new Set([
|
|
3
|
+
'read', 'grep', 'find', 'ls', 'todo_write', 'session_name',
|
|
4
|
+
'history_search', 'history_read', 'trajectory_search', 'trajectory_read',
|
|
5
|
+
'wiki_entrypoint', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load',
|
|
6
|
+
'web_search', 'web_fetch'
|
|
7
|
+
]);
|
|
8
|
+
export const PLAN_RESEARCH_PLUGINS = ['history', 'variant', 'web', 'wiki'];
|
|
9
|
+
export const PLAN_RESEARCH_EVIDENCE_TOOLS = new Set([
|
|
10
|
+
'read', 'grep', 'find', 'ls', 'history_search', 'trajectory_search', 'wiki_search', 'web_search', 'web_fetch'
|
|
11
|
+
]);
|
|
12
|
+
function profileRefusal(profile) {
|
|
13
|
+
if (!profile)
|
|
14
|
+
return 'Plan-mode delegation requires a named, eligible read-only research profile.';
|
|
15
|
+
if (!Array.isArray(profile.tools) || profile.tools.length === 0) {
|
|
16
|
+
return `Profile ${profile.name} must declare an explicit tools allowlist for plan-mode research.`;
|
|
17
|
+
}
|
|
18
|
+
const unsafeTools = profile.tools.filter(name => !PLAN_RESEARCH_TOOLS.has(name));
|
|
19
|
+
if (unsafeTools.length) {
|
|
20
|
+
return `Profile ${profile.name} includes non-research tools: ${[...new Set(unsafeTools)].join(', ')}. Use a profile whose tools are all plan research tools.`;
|
|
21
|
+
}
|
|
22
|
+
if (!profile.tools.some(name => PLAN_RESEARCH_EVIDENCE_TOOLS.has(name))) {
|
|
23
|
+
return `Profile ${profile.name} must allow at least one read-only research evidence tool.`;
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(profile.plugins)) {
|
|
26
|
+
return `Profile ${profile.name} must declare an explicit plugin allowlist for plan-mode research.`;
|
|
27
|
+
}
|
|
28
|
+
const unsafePlugins = profile.plugins.filter(name => !PLAN_RESEARCH_PLUGINS.includes(name));
|
|
29
|
+
if (unsafePlugins.length) {
|
|
30
|
+
return `Profile ${profile.name} includes non-research plugins: ${[...new Set(unsafePlugins)].join(', ')}. Use only the plan research plugins: ${PLAN_RESEARCH_PLUGINS.join(', ')}.`;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** Whether the profile's explicit allowlists are restricted to read-only research. */
|
|
35
|
+
export function planResearchProfile(profile) {
|
|
36
|
+
return profileRefusal(profile) === null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Evaluate the same effective intersection used for launch, including tool ownership removed by
|
|
40
|
+
* profile plugin narrowing. An explicit request is a strict override; omission inherits the safe
|
|
41
|
+
* profile/parent intersection.
|
|
42
|
+
*/
|
|
43
|
+
export function planResearchEligibility(profile, context, requested) {
|
|
44
|
+
const empty = { version: 1, tools: [], connectors: [], contributions: [] };
|
|
45
|
+
const profileProblem = profileRefusal(profile);
|
|
46
|
+
if (profileProblem)
|
|
47
|
+
return { eligible: false, refusal: profileProblem, capabilities: empty, unavailableTools: [] };
|
|
48
|
+
const parent = {
|
|
49
|
+
version: 1,
|
|
50
|
+
tools: [...context.frozenTools],
|
|
51
|
+
connectors: [],
|
|
52
|
+
contributions: [...context.contributions]
|
|
53
|
+
};
|
|
54
|
+
const narrowed = narrowForProfile(parent, profile, requested, context.descriptors);
|
|
55
|
+
const capabilities = {
|
|
56
|
+
...narrowed.capabilities,
|
|
57
|
+
tools: narrowed.capabilities.tools.filter(name => PLAN_RESEARCH_TOOLS.has(name)),
|
|
58
|
+
connectors: []
|
|
59
|
+
};
|
|
60
|
+
if (requested) {
|
|
61
|
+
const effective = new Set(capabilities.tools);
|
|
62
|
+
const incompatible = [...new Set(requested.filter(name => !PLAN_RESEARCH_TOOLS.has(name)
|
|
63
|
+
|| !profile.tools.includes(name)
|
|
64
|
+
|| !context.frozenTools.includes(name)
|
|
65
|
+
|| !effective.has(name)))];
|
|
66
|
+
if (incompatible.length) {
|
|
67
|
+
return {
|
|
68
|
+
eligible: false,
|
|
69
|
+
refusal: `Plan-mode tools override contains incompatible tool(s): ${incompatible.join(', ')}. Omit \`tools\` to inherit the selected profile's safe research intersection, or retry with only tools allowed by that profile, available in this parent run, and in the plan research set.`,
|
|
70
|
+
capabilities,
|
|
71
|
+
unavailableTools: narrowed.unavailable
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!capabilities.tools.some(name => PLAN_RESEARCH_EVIDENCE_TOOLS.has(name))) {
|
|
76
|
+
const refusal = requested
|
|
77
|
+
? `The tools override [${requested.join(', ')}] leaves no available research evidence tool for profile ${profile.name}. Omit \`tools\` to inherit the safe intersection, or retry with an available evidence tool from this profile.`
|
|
78
|
+
: `Profile ${profile.name} has no read-only research evidence tool after intersecting its allowlists with this run's frozen capabilities and plugin contributions. Choose a profile with an available evidence tool.`;
|
|
79
|
+
return { eligible: false, refusal, capabilities, unavailableTools: narrowed.unavailable };
|
|
80
|
+
}
|
|
81
|
+
return { eligible: true, refusal: null, capabilities, unavailableTools: narrowed.unavailable };
|
|
82
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type Effort } from "../core/effort.js";
|
|
2
|
+
import { type PlanResearchContext } from "./planResearch.js";
|
|
2
3
|
export declare const PROFILE_NAME: RegExp;
|
|
4
|
+
/** Names a profile cannot take: `main` is how rosters show the agent a person talks to. */
|
|
5
|
+
export declare const RESERVED_PROFILE_NAMES: readonly string[];
|
|
3
6
|
export declare const PROFILE_COLORS: readonly ["blue", "green", "yellow", "red", "magenta", "cyan", "orange", "purple"];
|
|
4
7
|
export type ProfileColor = (typeof PROFILE_COLORS)[number];
|
|
5
8
|
export type ProfileSource = 'builtin' | 'global' | 'project' | 'variant';
|
|
@@ -71,10 +74,11 @@ export declare function removeProfile(root: string, cwd: string | null | undefin
|
|
|
71
74
|
/** Where the profile list sits among the prompt sections: the slot a variant's subagents used. */
|
|
72
75
|
export declare const PROFILE_SECTION_ORDER = 64;
|
|
73
76
|
/** The prompt section that lists the profiles a run can start subagents from. */
|
|
74
|
-
export declare function profileSection(root: string, cwd: string | null | undefined, variantDir?: string | null): {
|
|
77
|
+
export declare function profileSection(root: string, cwd: string | null | undefined, variantDir?: string | null, planResearchOnly?: boolean, researchContext?: PlanResearchContext): {
|
|
75
78
|
id: string;
|
|
76
79
|
title: string;
|
|
77
80
|
body: string;
|
|
78
81
|
budget: number;
|
|
79
82
|
order: number;
|
|
83
|
+
dynamic: true;
|
|
80
84
|
};
|