@sema-agent/core 2.12.0 → 3.0.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/dist/agents/agent-transcript-tool.d.ts +1 -0
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/agents/cascade.d.ts +4 -0
- package/dist/agents/cascade.js +54 -9
- package/dist/agents/repair-loop.d.ts +2 -0
- package/dist/agents/repair-loop.js +101 -2
- package/dist/agents/retain-ledger.d.ts +18 -2
- package/dist/agents/retain-ledger.js +37 -8
- package/dist/agents/roster-store.d.ts +4 -0
- package/dist/agents/roster-store.js +22 -1
- package/dist/agents/teacher.js +4 -2
- package/dist/agents/team.js +7 -4
- package/dist/agents/verify.d.ts +15 -2
- package/dist/agents/verify.js +36 -1
- package/dist/bin/sema-tb.js +2 -2
- package/dist/brain/circuit-breaker.d.ts +1 -0
- package/dist/brain/circuit-breaker.js +26 -9
- package/dist/brain/degrading.js +13 -1
- package/dist/brain/failover.js +2 -0
- package/dist/brain/repetition.js +10 -1
- package/dist/brain/routing.js +5 -1
- package/dist/brain/status-sink.js +5 -1
- package/dist/brain/stream-engine.js +4 -1
- package/dist/brain/tool-call-repair.js +32 -14
- package/dist/core/auto-compaction.js +17 -7
- package/dist/core/auto-promote.js +2 -1
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/checkpoint-store.js +28 -10
- package/dist/core/consolidate-scope.js +4 -3
- package/dist/core/context-edit.js +3 -0
- package/dist/core/context-guard.js +3 -0
- package/dist/core/git-worktree-env.d.ts +6 -1
- package/dist/core/git-worktree-env.js +17 -1
- package/dist/core/hooks.d.ts +1 -0
- package/dist/core/hooks.js +40 -2
- package/dist/core/lsp-diagnostics.js +13 -7
- package/dist/core/lsp-protocol.d.ts +1 -1
- package/dist/core/lsp-protocol.js +20 -8
- package/dist/core/lsp-session.d.ts +12 -2
- package/dist/core/lsp-session.js +114 -49
- package/dist/core/lsp.d.ts +13 -1
- package/dist/core/lsp.js +64 -9
- package/dist/core/mcp.d.ts +1 -0
- package/dist/core/mcp.js +4 -3
- package/dist/core/memory-engine/data-plane.js +4 -0
- package/dist/core/memory-engine/engine.d.ts +2 -1
- package/dist/core/memory-engine/engine.js +82 -7
- package/dist/core/memory-engine/file-backend.d.ts +1 -0
- package/dist/core/memory-engine/file-backend.js +13 -3
- package/dist/core/memory-engine/layout.js +12 -1
- package/dist/core/memory.js +3 -0
- package/dist/core/permission-rules.d.ts +2 -1
- package/dist/core/permission-rules.js +24 -3
- package/dist/core/runner/active-skill-scope.js +34 -6
- package/dist/core/runner/assemble-result.d.ts +1 -1
- package/dist/core/runner/assemble-result.js +0 -1
- package/dist/core/runner/prepare-task.js +23 -3
- package/dist/core/runner/runtask.js +48 -30
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +26 -7
- package/dist/core/runner/usage-accounting.d.ts +6 -0
- package/dist/core/runner/usage-accounting.js +9 -8
- package/dist/core/secret-env.js +5 -1
- package/dist/core/sensitive-path-policy.js +9 -3
- package/dist/core/session-store.js +15 -4
- package/dist/core/skill-tool-specifier.d.ts +8 -0
- package/dist/core/skill-tool-specifier.js +58 -0
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +16 -4
- package/dist/core/store-contracts/session-repo-contract.js +40 -0
- package/dist/core/task-registry-monitor.js +7 -1
- package/dist/core/tool-policy.js +65 -17
- package/dist/core/trace.d.ts +1 -0
- package/dist/core/types.d.ts +3 -5
- package/dist/core/with-retry.js +0 -1
- package/dist/engine/compaction/compaction.d.ts +1 -0
- package/dist/engine/compaction/compaction.js +14 -5
- package/dist/engine/lsp/frame-decoder.js +6 -3
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +103 -26
- package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
- package/dist/index.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/workflow-governance.js +43 -9
- package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
- package/dist/orchestration/workflow-script-store.js +34 -4
- package/dist/orchestration/workflow.js +10 -6
- package/dist/prompt-assembly/artifact-store.d.ts +2 -0
- package/dist/prompt-assembly/artifact-store.js +39 -24
- package/dist/prompt-assembly/assemble.js +38 -7
- package/dist/prompt-assembly/epoch.js +19 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/explain.js +1 -1
- package/dist/prompt-assembly/tool-catalog.js +16 -1
- package/dist/prompt-assembly/turn-snapshot.js +4 -1
- package/dist/stores/file/fs-atomic.d.ts +3 -0
- package/dist/stores/file/fs-atomic.js +53 -8
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +7 -0
- package/dist/stores/file/session-store.d.ts +2 -0
- package/dist/stores/file/session-store.js +28 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
- package/dist/tools/fs/bash-readonly-classifier.js +57 -8
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +93 -5
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/monitor.js +55 -9
- package/dist/tools/scheduler-tools.js +9 -3
- package/dist/tools/web.d.ts +8 -2
- package/dist/tools/web.js +46 -17
- package/dist/tools/worktree.d.ts +2 -0
- package/dist/tools/worktree.js +233 -143
- package/package.json +5 -2
|
@@ -20,12 +20,29 @@ const PROBE_FACTS_OFF = {
|
|
|
20
20
|
teammateEnabled: false,
|
|
21
21
|
orchestrationDeferred: false,
|
|
22
22
|
isSubagent: false,
|
|
23
|
+
promptProfile: "simple",
|
|
24
|
+
fableMitigations: false,
|
|
25
|
+
};
|
|
26
|
+
const PROBE_FACTS_ON = {
|
|
27
|
+
policyEnabled: true,
|
|
28
|
+
hooksEnabled: true,
|
|
29
|
+
isolationEnabled: true,
|
|
30
|
+
withinTaskCompactionEnabled: true,
|
|
31
|
+
supervisorEnabled: true,
|
|
32
|
+
orchestrationEnabled: true,
|
|
33
|
+
awarenessEnabled: true,
|
|
34
|
+
goalEnabled: true,
|
|
35
|
+
worktreeIsolated: true,
|
|
36
|
+
teammateEnabled: true,
|
|
37
|
+
orchestrationDeferred: true,
|
|
38
|
+
isSubagent: true,
|
|
39
|
+
promptProfile: "classic",
|
|
40
|
+
fableMitigations: true,
|
|
23
41
|
};
|
|
24
|
-
const PROBE_FACTS_ON = Object.fromEntries(Object.keys(PROBE_FACTS_OFF).map((k) => [k, true]));
|
|
25
42
|
const PROBE_VECTORS = [
|
|
26
43
|
PROBE_FACTS_OFF,
|
|
27
44
|
PROBE_FACTS_ON,
|
|
28
|
-
...Object.keys(PROBE_FACTS_OFF).map((k) => ({ ...PROBE_FACTS_OFF, [k]:
|
|
45
|
+
...Object.keys(PROBE_FACTS_OFF).map((k) => ({ ...PROBE_FACTS_OFF, [k]: PROBE_FACTS_ON[k] })),
|
|
29
46
|
];
|
|
30
47
|
const PROBE_INPUTS_BASE = {
|
|
31
48
|
roleBase: "\u0000probe-role\u0000",
|
|
@@ -4,7 +4,7 @@ export const EVENT_PROMPT_REGISTRY = new Map([
|
|
|
4
4
|
{ kind: "changed_files", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderChangedFiles" },
|
|
5
5
|
{ kind: "plan_mode", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#PLAN_MODE_FULL_BODY" },
|
|
6
6
|
{ kind: "date_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "always", rendererRef: "turn-attachments.ts#renderDateChange" },
|
|
7
|
-
{ kind: "instructions_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", maxBytes: 512, defaultPolicy: "always", rendererRef: "turn-attachments.ts#
|
|
7
|
+
{ kind: "instructions_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", maxBytes: 512, defaultPolicy: "always", rendererRef: "turn-attachments.ts#collectInstructionsChange" },
|
|
8
8
|
{ kind: "budget_usd", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderBudgetUsd" },
|
|
9
9
|
{ kind: "background_tasks", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderBackgroundTasks" },
|
|
10
10
|
{ kind: "tools_delta", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderToolsDelta" },
|
|
@@ -12,8 +12,8 @@ export const EVENT_PROMPT_REGISTRY = new Map([
|
|
|
12
12
|
{ kind: "skills_listing", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderSkillsListingDelta" },
|
|
13
13
|
{ kind: "mcp_instructions", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderMcpInstructionsDelta" },
|
|
14
14
|
{ kind: "mcp_dropped_tools", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderMcpDroppedTools" },
|
|
15
|
-
{ kind: "deadline_converge", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#
|
|
16
|
-
{ kind: "deadline_deliver_now", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#
|
|
15
|
+
{ kind: "deadline_converge", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#nudgeSchedule" },
|
|
16
|
+
{ kind: "deadline_deliver_now", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#nudgeSchedule" },
|
|
17
17
|
{ kind: "deadline_finalize", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#buildFinalizeText" },
|
|
18
18
|
{ kind: "final_verification", carrier: "message.user-prefix", trust: "operator", dedupe: "once-per-run", defaultPolicy: "always", rendererRef: "runtask.ts#final-verification" },
|
|
19
19
|
].map((e) => [e.kind, e]));
|
|
@@ -30,7 +30,7 @@ export function describeDefaultPack() {
|
|
|
30
30
|
return {
|
|
31
31
|
packId: SEMA_DEFAULT_PACK.packId,
|
|
32
32
|
roleBaseText: DEFAULT_SYSTEM_PROMPT,
|
|
33
|
-
sections: SEMA_DEFAULT_PACK.sections.map((s) => ({
|
|
33
|
+
sections: [...SEMA_DEFAULT_PACK.sections].sort((a, b) => a.rank - b.rank).map((s) => ({
|
|
34
34
|
id: s.id,
|
|
35
35
|
slot: s.slot,
|
|
36
36
|
rank: s.rank,
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
const PRESENTATION_KEYWORDS = new Set(["description", "title", "examples", "$id"]);
|
|
3
|
+
const SCHEMA_NAME_MAPS = new Set([
|
|
4
|
+
"properties",
|
|
5
|
+
"patternProperties",
|
|
6
|
+
"definitions",
|
|
7
|
+
"$defs",
|
|
8
|
+
"dependentSchemas",
|
|
9
|
+
]);
|
|
2
10
|
function stripPresentation(node) {
|
|
3
11
|
if (Array.isArray(node))
|
|
4
12
|
return node.map(stripPresentation);
|
|
@@ -6,8 +14,15 @@ function stripPresentation(node) {
|
|
|
6
14
|
return node;
|
|
7
15
|
const out = {};
|
|
8
16
|
for (const [k, v] of Object.entries(node)) {
|
|
9
|
-
if (k
|
|
17
|
+
if (PRESENTATION_KEYWORDS.has(k))
|
|
10
18
|
continue;
|
|
19
|
+
if (SCHEMA_NAME_MAPS.has(k) && v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
20
|
+
const named = {};
|
|
21
|
+
for (const [name, sub] of Object.entries(v))
|
|
22
|
+
named[name] = stripPresentation(sub);
|
|
23
|
+
out[k] = named;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
11
26
|
out[k] = k === "required" && Array.isArray(v) ? [...v].sort() : stripPresentation(v);
|
|
12
27
|
}
|
|
13
28
|
return out;
|
|
@@ -61,7 +61,10 @@ export function buildTurnPromptSnapshot(raw, api, promptHasher) {
|
|
|
61
61
|
loweringVersion: `${lowering.version}/${lowering.form}/${api}`,
|
|
62
62
|
};
|
|
63
63
|
const bump = () => {
|
|
64
|
-
|
|
64
|
+
const next = identityOf(elements);
|
|
65
|
+
if (next === snapshot.cacheIdentity)
|
|
66
|
+
return;
|
|
67
|
+
snapshot.cacheIdentity = next;
|
|
65
68
|
snapshot.onChange?.(snapshot);
|
|
66
69
|
};
|
|
67
70
|
const snapshot = {
|
|
@@ -10,7 +10,10 @@ export declare class AppendLog {
|
|
|
10
10
|
private fd;
|
|
11
11
|
private disposed;
|
|
12
12
|
private readonly path;
|
|
13
|
+
private boundary;
|
|
13
14
|
constructor(path: string);
|
|
15
|
+
private sizeOrUnknown;
|
|
16
|
+
private repairTornTail;
|
|
14
17
|
append(record: unknown, fsync: boolean): void;
|
|
15
18
|
close(): void;
|
|
16
19
|
closeForSwap(): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, constants as FS, existsSync, linkSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, truncateSync, unlinkSync, writeFileSync, writeSync, fsyncSync, } from "node:fs";
|
|
1
|
+
import { closeSync, constants as FS, existsSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, truncateSync, unlinkSync, writeFileSync, writeSync, fsyncSync, } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -148,30 +148,75 @@ export class AppendLog {
|
|
|
148
148
|
fd;
|
|
149
149
|
disposed = false;
|
|
150
150
|
path;
|
|
151
|
+
boundary = -1;
|
|
151
152
|
constructor(path) {
|
|
152
153
|
this.path = path;
|
|
153
154
|
ensureDir(dirnameOf(path));
|
|
154
155
|
truncateTornTail(path);
|
|
155
|
-
this.fd = openSync(path, "a", 0o600);
|
|
156
|
+
this.fd = openSync(path, "a+", 0o600);
|
|
157
|
+
this.boundary = this.sizeOrUnknown();
|
|
158
|
+
}
|
|
159
|
+
sizeOrUnknown() {
|
|
160
|
+
try {
|
|
161
|
+
return fstatSync(this.fd).size;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return -1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
repairTornTail() {
|
|
168
|
+
const size = this.sizeOrUnknown();
|
|
169
|
+
if (size < 0) {
|
|
170
|
+
this.boundary = -1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (size === this.boundary)
|
|
174
|
+
return;
|
|
175
|
+
if (size === 0) {
|
|
176
|
+
this.boundary = 0;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const tail = Buffer.allocUnsafe(1);
|
|
180
|
+
try {
|
|
181
|
+
if (readSync(this.fd, tail, 0, 1, size - 1) === 1 && tail[0] === 0x0a) {
|
|
182
|
+
this.boundary = size;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
truncateTornTail(this.path);
|
|
190
|
+
this.boundary = this.sizeOrUnknown();
|
|
156
191
|
}
|
|
157
192
|
append(record, fsync) {
|
|
158
193
|
if (this.fd < 0 && !this.disposed) {
|
|
159
194
|
truncateTornTail(this.path);
|
|
160
|
-
this.fd = openSync(this.path, "a", 0o600);
|
|
195
|
+
this.fd = openSync(this.path, "a+", 0o600);
|
|
196
|
+
this.boundary = this.sizeOrUnknown();
|
|
161
197
|
}
|
|
162
198
|
if (this.fd < 0) {
|
|
163
199
|
const e = new Error("append: this log was closed (its session/box was deleted or replaced) — refusing to write through a released descriptor");
|
|
164
200
|
e.code = "log_closed";
|
|
165
201
|
throw e;
|
|
166
202
|
}
|
|
203
|
+
this.repairTornTail();
|
|
167
204
|
const buf = Buffer.from(`${JSON.stringify(record)}\n`, "utf8");
|
|
168
205
|
let written = 0;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
206
|
+
try {
|
|
207
|
+
while (written < buf.length) {
|
|
208
|
+
const n = writeSync(this.fd, buf, written, buf.length - written);
|
|
209
|
+
if (n <= 0)
|
|
210
|
+
throw new Error(`append: write made no progress (${written}/${buf.length} bytes) — refusing to leave a torn record`);
|
|
211
|
+
written += n;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
this.boundary = -1;
|
|
216
|
+
throw err;
|
|
174
217
|
}
|
|
218
|
+
if (this.boundary >= 0)
|
|
219
|
+
this.boundary += buf.length;
|
|
175
220
|
if (fsync)
|
|
176
221
|
fsyncSync(this.fd);
|
|
177
222
|
}
|
|
@@ -38,6 +38,7 @@ export declare class FileStorageBackend {
|
|
|
38
38
|
private readonly fileCheckpoints;
|
|
39
39
|
private readonly fileMemory;
|
|
40
40
|
private readonly ttl;
|
|
41
|
+
private readonly fileSessions;
|
|
41
42
|
private readonly fileWorkflowJournal;
|
|
42
43
|
constructor(opts?: FileStorageBackendOptions);
|
|
43
44
|
dispose(): Promise<void>;
|
|
@@ -32,6 +32,7 @@ export class FileStorageBackend {
|
|
|
32
32
|
fileCheckpoints;
|
|
33
33
|
fileMemory;
|
|
34
34
|
ttl;
|
|
35
|
+
fileSessions;
|
|
35
36
|
fileWorkflowJournal;
|
|
36
37
|
constructor(opts = {}) {
|
|
37
38
|
this.root = resolveDataRoot(opts.root);
|
|
@@ -39,6 +40,7 @@ export class FileStorageBackend {
|
|
|
39
40
|
this.lock.acquire();
|
|
40
41
|
try {
|
|
41
42
|
const repo = new FileSessionRepo(this.root);
|
|
43
|
+
this.fileSessions = repo;
|
|
42
44
|
this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget" });
|
|
43
45
|
this.sessionStore = this.ttl;
|
|
44
46
|
this.fileCheckpoints = new FileCheckpointStore(this.root, opts.checkpoint);
|
|
@@ -63,6 +65,11 @@ export class FileStorageBackend {
|
|
|
63
65
|
}
|
|
64
66
|
catch {
|
|
65
67
|
}
|
|
68
|
+
try {
|
|
69
|
+
await this.fileSessions.dispose();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
}
|
|
66
73
|
this.fileCheckpoints.close();
|
|
67
74
|
try {
|
|
68
75
|
await this.fileMemory.flushEmbeds();
|
|
@@ -2,10 +2,12 @@ import type { Session, SessionMetadata, SessionRepo, SessionTreeEntry } from "..
|
|
|
2
2
|
export declare class FileSessionRepo implements SessionRepo {
|
|
3
3
|
private readonly dir;
|
|
4
4
|
private readonly tmpDir;
|
|
5
|
+
private readonly joined;
|
|
5
6
|
constructor(root: string);
|
|
6
7
|
private pathFor;
|
|
7
8
|
private read;
|
|
8
9
|
private storage;
|
|
10
|
+
dispose(): Promise<void>;
|
|
9
11
|
create(options?: {
|
|
10
12
|
id?: string;
|
|
11
13
|
}): Promise<Session>;
|
|
@@ -20,11 +20,19 @@ function evictSharedSessionStorage(canonical) {
|
|
|
20
20
|
class FileSessionStorage extends BaseSessionStorage {
|
|
21
21
|
log;
|
|
22
22
|
canonicalPath;
|
|
23
|
+
holders = new Set();
|
|
23
24
|
constructor(log, meta, entries, canonicalPath) {
|
|
24
25
|
super(meta, entries);
|
|
25
26
|
this.log = log;
|
|
26
27
|
this.canonicalPath = canonicalPath;
|
|
27
28
|
}
|
|
29
|
+
addHolder(holder) {
|
|
30
|
+
this.holders.add(holder);
|
|
31
|
+
}
|
|
32
|
+
removeHolder(holder) {
|
|
33
|
+
this.holders.delete(holder);
|
|
34
|
+
return this.holders.size === 0;
|
|
35
|
+
}
|
|
28
36
|
closeLog() {
|
|
29
37
|
this.log.close();
|
|
30
38
|
}
|
|
@@ -46,6 +54,7 @@ class FileSessionStorage extends BaseSessionStorage {
|
|
|
46
54
|
export class FileSessionRepo {
|
|
47
55
|
dir;
|
|
48
56
|
tmpDir;
|
|
57
|
+
joined = new Map();
|
|
49
58
|
constructor(root) {
|
|
50
59
|
this.dir = join(root, "sessions");
|
|
51
60
|
this.tmpDir = join(root, "tmp");
|
|
@@ -77,19 +86,37 @@ export class FileSessionRepo {
|
|
|
77
86
|
storage(id, createdAt, entries, forkedFrom) {
|
|
78
87
|
const canonical = canonicalStoreKey(this.pathFor(id));
|
|
79
88
|
const live = sharedSessionStorages.get(canonical)?.deref();
|
|
80
|
-
if (live !== undefined)
|
|
89
|
+
if (live !== undefined) {
|
|
90
|
+
live.addHolder(this);
|
|
91
|
+
this.joined.set(canonical, new WeakRef(live));
|
|
81
92
|
return live;
|
|
93
|
+
}
|
|
82
94
|
const log = new AppendLog(this.pathFor(id));
|
|
83
95
|
const created = new FileSessionStorage(log, { id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}) }, entries, canonical);
|
|
84
96
|
sharedSessionStorages.set(canonical, new WeakRef(created));
|
|
97
|
+
created.addHolder(this);
|
|
98
|
+
this.joined.set(canonical, new WeakRef(created));
|
|
85
99
|
sessionStorageFinalizer.register(created, { canonical, log }, created);
|
|
86
100
|
return created;
|
|
87
101
|
}
|
|
102
|
+
async dispose() {
|
|
103
|
+
for (const [canonical, ref] of this.joined) {
|
|
104
|
+
const mine = ref.deref();
|
|
105
|
+
if (mine === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
if (sharedSessionStorages.get(canonical)?.deref() !== mine)
|
|
108
|
+
continue;
|
|
109
|
+
if (mine.removeHolder(this))
|
|
110
|
+
evictSharedSessionStorage(canonical);
|
|
111
|
+
}
|
|
112
|
+
this.joined.clear();
|
|
113
|
+
}
|
|
88
114
|
async create(options = {}) {
|
|
89
115
|
const id = options.id ?? uuidv7();
|
|
90
116
|
const createdAt = new Date().toISOString();
|
|
91
117
|
const path = this.pathFor(id);
|
|
92
118
|
if (!existsSync(path)) {
|
|
119
|
+
evictSharedSessionStorage(canonicalStoreKey(path));
|
|
93
120
|
atomicWriteFile(this.tmpDir, path, `${JSON.stringify({ kind: "meta", id, createdAt })}\n`);
|
|
94
121
|
return new StoredSession(this.storage(id, createdAt, []));
|
|
95
122
|
}
|
|
@@ -14,6 +14,7 @@ export interface CompoundReadonlyVerdict {
|
|
|
14
14
|
reason?: string;
|
|
15
15
|
outOfRootRead?: true;
|
|
16
16
|
outOfRootPaths?: readonly string[];
|
|
17
|
+
checkedPaths?: readonly string[];
|
|
17
18
|
}
|
|
18
19
|
export declare function formatOutOfRootReadApprovalOption(directory: string): string;
|
|
19
20
|
export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
|
|
@@ -107,10 +107,42 @@ function hasUnquotedExpansionMetachar(rawToken) {
|
|
|
107
107
|
}
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
|
+
function longOptionNameOf(tok) {
|
|
111
|
+
if (!tok.startsWith("--") || tok.length === 2)
|
|
112
|
+
return undefined;
|
|
113
|
+
const body = tok.slice(2);
|
|
114
|
+
const eq = body.indexOf("=");
|
|
115
|
+
const name = eq >= 0 ? body.slice(0, eq) : body;
|
|
116
|
+
return name.length > 0 ? name : undefined;
|
|
117
|
+
}
|
|
118
|
+
function isLongOptionAbbrevOf(name, full) {
|
|
119
|
+
return full.startsWith(name);
|
|
120
|
+
}
|
|
110
121
|
function isGrepPatternFlagToken(tok) {
|
|
111
|
-
if (tok.startsWith("--"))
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
if (tok.startsWith("--")) {
|
|
123
|
+
const long = longOptionNameOf(tok);
|
|
124
|
+
return long !== undefined && (isLongOptionAbbrevOf(long, "regexp") || isLongOptionAbbrevOf(long, "file"));
|
|
125
|
+
}
|
|
126
|
+
return /^-[A-Za-z0-9]*[ef]/.test(tok);
|
|
127
|
+
}
|
|
128
|
+
function isGrepPatternPayloadLongOption(tok) {
|
|
129
|
+
if (!tok.includes("="))
|
|
130
|
+
return false;
|
|
131
|
+
const name = longOptionNameOf(tok);
|
|
132
|
+
return name !== undefined && isLongOptionAbbrevOf(name, "regexp");
|
|
133
|
+
}
|
|
134
|
+
function isCutDelimiterPayloadLongOption(tok) {
|
|
135
|
+
if (!tok.includes("="))
|
|
136
|
+
return false;
|
|
137
|
+
const name = longOptionNameOf(tok);
|
|
138
|
+
return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
|
|
139
|
+
}
|
|
140
|
+
function isGrepFileStdinLongOption(tok) {
|
|
141
|
+
const eq = tok.indexOf("=");
|
|
142
|
+
if (eq < 0 || tok.slice(eq + 1) !== "-")
|
|
143
|
+
return false;
|
|
144
|
+
const name = longOptionNameOf(tok);
|
|
145
|
+
return name !== undefined && isLongOptionAbbrevOf(name, "file");
|
|
114
146
|
}
|
|
115
147
|
function grepClusterValueOwner(tok) {
|
|
116
148
|
if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
|
|
@@ -147,6 +179,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
147
179
|
const args = tokens.folded.slice(1);
|
|
148
180
|
const findings = [];
|
|
149
181
|
const candidates = [];
|
|
182
|
+
const bareWordOperands = [];
|
|
150
183
|
for (const rawArg of tokens.raw.slice(1)) {
|
|
151
184
|
if (!hasUnquotedExpansionMetachar(rawArg))
|
|
152
185
|
continue;
|
|
@@ -176,9 +209,9 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
176
209
|
k++;
|
|
177
210
|
continue;
|
|
178
211
|
}
|
|
179
|
-
if (name === "cut" && (/^-d./.test(t) ||
|
|
212
|
+
if (name === "cut" && (/^-d./.test(t) || isCutDelimiterPayloadLongOption(t)))
|
|
180
213
|
continue;
|
|
181
|
-
if (name === "grep" && (grepClusterValueOwner(t) === "e" ||
|
|
214
|
+
if (name === "grep" && (grepClusterValueOwner(t) === "e" || isGrepPatternPayloadLongOption(t)))
|
|
182
215
|
continue;
|
|
183
216
|
if (t.startsWith("-") && t !== "-") {
|
|
184
217
|
for (const payload of attachedOptionPayloads(t)) {
|
|
@@ -195,6 +228,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
195
228
|
continue;
|
|
196
229
|
if (isPathShapedToken(t))
|
|
197
230
|
candidates.push(t);
|
|
231
|
+
else
|
|
232
|
+
bareWordOperands.push(t);
|
|
198
233
|
}
|
|
199
234
|
}
|
|
200
235
|
for (const candidate of candidates) {
|
|
@@ -206,9 +241,16 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
206
241
|
});
|
|
207
242
|
continue;
|
|
208
243
|
}
|
|
209
|
-
if (
|
|
244
|
+
if (withinAnyRoot(boundary.roots, resolved))
|
|
245
|
+
findings.push({ kind: "inside", path: resolved });
|
|
246
|
+
else
|
|
210
247
|
findings.push({ kind: "outside", command: name, path: resolved });
|
|
211
248
|
}
|
|
249
|
+
for (const operand of bareWordOperands) {
|
|
250
|
+
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
251
|
+
if (resolved !== undefined && withinAnyRoot(boundary.roots, resolved))
|
|
252
|
+
findings.push({ kind: "inside", path: resolved });
|
|
253
|
+
}
|
|
212
254
|
return findings;
|
|
213
255
|
}
|
|
214
256
|
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
@@ -312,7 +354,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
312
354
|
k++;
|
|
313
355
|
continue;
|
|
314
356
|
}
|
|
315
|
-
if (name === "grep" && (t === "-f-" || t
|
|
357
|
+
if (name === "grep" && (t === "-f-" || isGrepFileStdinLongOption(t))) {
|
|
316
358
|
hasStdinDash = true;
|
|
317
359
|
break;
|
|
318
360
|
}
|
|
@@ -351,22 +393,29 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
351
393
|
}
|
|
352
394
|
function evaluateReadBoundary(foldedSegments, boundary) {
|
|
353
395
|
const outside = [];
|
|
396
|
+
const inside = [];
|
|
354
397
|
for (const toks of foldedSegments) {
|
|
355
398
|
for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
|
|
356
399
|
if (finding.kind === "unresolvable")
|
|
357
400
|
return { reason: finding.reason };
|
|
401
|
+
if (finding.kind === "inside") {
|
|
402
|
+
if (!inside.includes(finding.path))
|
|
403
|
+
inside.push(finding.path);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
358
406
|
if (!outside.some((o) => o.path === finding.path))
|
|
359
407
|
outside.push(finding);
|
|
360
408
|
}
|
|
361
409
|
}
|
|
362
410
|
if (outside.length === 0)
|
|
363
|
-
return {};
|
|
411
|
+
return inside.length > 0 ? { checkedPaths: inside } : {};
|
|
364
412
|
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
365
413
|
const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
|
|
366
414
|
return {
|
|
367
415
|
reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
|
|
368
416
|
outOfRootRead: true,
|
|
369
417
|
outOfRootPaths: outside.map((o) => o.path),
|
|
418
|
+
...(inside.length > 0 ? { checkedPaths: inside } : {}),
|
|
370
419
|
};
|
|
371
420
|
}
|
|
372
421
|
export function classifySimpleCommandReadBoundary(command, boundary) {
|
|
@@ -14,7 +14,7 @@ export interface ExecClampOption {
|
|
|
14
14
|
}) => () => void;
|
|
15
15
|
}
|
|
16
16
|
export declare function bashExitOneInterpretation(command: string): string | undefined;
|
|
17
|
-
export declare function canAutoBackground(command: string): boolean;
|
|
17
|
+
export declare function canAutoBackground(command: string, depth?: number): boolean;
|
|
18
18
|
export declare function createBashTool(env: ExecutionEnv, rootCanonical: string, coAuthor?: string | false, cwdRef?: CwdRef, taskOpts?: {
|
|
19
19
|
taskRegistry?: TaskRegistry;
|
|
20
20
|
taskOwner?: string;
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -6,6 +6,7 @@ import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
|
6
6
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
7
7
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
8
8
|
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
9
|
+
import { isRemoteExecutionEnv } from "../../core/remote-env.js";
|
|
9
10
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
10
11
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
|
|
11
12
|
import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
|
|
@@ -137,7 +138,26 @@ function commandBasename(token) {
|
|
|
137
138
|
const idx = token.lastIndexOf("/");
|
|
138
139
|
return idx === -1 ? token : token.slice(idx + 1);
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
+
const COMMAND_STRING_SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash"]);
|
|
142
|
+
function commandStringPayload(tokens) {
|
|
143
|
+
const word = commandBasename(tokens[0] ?? "");
|
|
144
|
+
if (word === "eval")
|
|
145
|
+
return tokens.slice(1).join(" ");
|
|
146
|
+
if (!COMMAND_STRING_SHELLS.has(word))
|
|
147
|
+
return undefined;
|
|
148
|
+
const flagIdx = tokens.findIndex((t, i) => i > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(t));
|
|
149
|
+
return flagIdx === -1 ? undefined : tokens.slice(flagIdx + 1).join(" ");
|
|
150
|
+
}
|
|
151
|
+
function unquotePayload(payload) {
|
|
152
|
+
const s = payload.trim();
|
|
153
|
+
const ansiC = /^\$'([\s\S]*)'$/.exec(s);
|
|
154
|
+
if (ansiC)
|
|
155
|
+
return (ansiC[1] ?? "").replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
|
156
|
+
const quoted = /^(['"])([\s\S]*)\1$/.exec(s);
|
|
157
|
+
return quoted ? (quoted[2] ?? "") : s;
|
|
158
|
+
}
|
|
159
|
+
const AUTO_BACKGROUND_MAX_WRAPPER_DEPTH = 3;
|
|
160
|
+
export function canAutoBackground(command, depth = 0) {
|
|
141
161
|
const segments = command
|
|
142
162
|
.split(/&&|\|\||;|\||\r?\n/)
|
|
143
163
|
.map((s) => s.trim())
|
|
@@ -149,6 +169,9 @@ export function canAutoBackground(command) {
|
|
|
149
169
|
const word = commandBasename(tokens[0] ?? "");
|
|
150
170
|
if (word === "git")
|
|
151
171
|
return false;
|
|
172
|
+
const payload = depth < AUTO_BACKGROUND_MAX_WRAPPER_DEPTH ? commandStringPayload(tokens) : undefined;
|
|
173
|
+
if (payload !== undefined && payload.trim() !== "" && !canAutoBackground(unquotePayload(payload), depth + 1))
|
|
174
|
+
return false;
|
|
152
175
|
if (word === "xargs" && tokens.slice(1).some((t) => commandBasename(t) === "git"))
|
|
153
176
|
return false;
|
|
154
177
|
}
|
|
@@ -641,6 +664,60 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
641
664
|
},
|
|
642
665
|
});
|
|
643
666
|
}
|
|
667
|
+
function createCanonicalReadBoundary(env, readRoots) {
|
|
668
|
+
const lexicalOnly = isRemoteExecutionEnv(env);
|
|
669
|
+
let rootsOnce;
|
|
670
|
+
const canonicalRoots = async (signal) => {
|
|
671
|
+
rootsOnce ??= (async () => {
|
|
672
|
+
const out = [];
|
|
673
|
+
for (const r of readRoots) {
|
|
674
|
+
const c = await env.canonicalPath(r, signal);
|
|
675
|
+
out.push(c.ok ? c.value : r);
|
|
676
|
+
}
|
|
677
|
+
return out;
|
|
678
|
+
})();
|
|
679
|
+
return await rootsOnce;
|
|
680
|
+
};
|
|
681
|
+
const resolve = async (p, signal) => {
|
|
682
|
+
const parts = p.split(/[\\/]/);
|
|
683
|
+
const tail = [];
|
|
684
|
+
while (parts.length > 0) {
|
|
685
|
+
const canon = await env.canonicalPath(parts.join("/") || "/", signal);
|
|
686
|
+
if (canon.ok) {
|
|
687
|
+
return [canon.value.replace(/[\\/]+$/, ""), ...[...tail].reverse()].join("/");
|
|
688
|
+
}
|
|
689
|
+
tail.push(parts.pop());
|
|
690
|
+
}
|
|
691
|
+
return undefined;
|
|
692
|
+
};
|
|
693
|
+
return {
|
|
694
|
+
escapesAfterResolution: async (paths, signal) => {
|
|
695
|
+
if (lexicalOnly || paths.length === 0)
|
|
696
|
+
return [];
|
|
697
|
+
const roots = await canonicalRoots(signal);
|
|
698
|
+
const escaping = [];
|
|
699
|
+
for (const p of paths) {
|
|
700
|
+
const real = await resolve(p, signal);
|
|
701
|
+
if (real === undefined)
|
|
702
|
+
continue;
|
|
703
|
+
if (!withinAnyRoot(roots, real) && !escaping.includes(real))
|
|
704
|
+
escaping.push(real);
|
|
705
|
+
}
|
|
706
|
+
return escaping;
|
|
707
|
+
},
|
|
708
|
+
allResolveInside: async (paths, signal) => {
|
|
709
|
+
if (lexicalOnly || paths.length === 0)
|
|
710
|
+
return false;
|
|
711
|
+
const roots = await canonicalRoots(signal);
|
|
712
|
+
for (const p of paths) {
|
|
713
|
+
const real = await resolve(p, signal);
|
|
714
|
+
if (real === undefined || !withinAnyRoot(roots, real))
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
}
|
|
644
721
|
export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opts) {
|
|
645
722
|
const timeoutCaps = resolveBashTimeoutCaps(opts);
|
|
646
723
|
const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
|
|
@@ -659,14 +736,16 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
|
|
|
659
736
|
}
|
|
660
737
|
return true;
|
|
661
738
|
};
|
|
739
|
+
const canonicalBoundary = createCanonicalReadBoundary(env, readRoots);
|
|
662
740
|
return defineTool({
|
|
663
741
|
name: "Bash",
|
|
664
742
|
contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
|
|
665
743
|
description: `Run a SINGLE read-only inspection command (e.g. ${sample}, …) and return its output. Shell ` +
|
|
666
744
|
"operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
|
|
667
745
|
"allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
|
|
668
|
-
"refused, as is one that cannot be resolved statically (e.g. a `~` path).
|
|
669
|
-
"
|
|
746
|
+
"refused, as is one that cannot be resolved statically (e.g. a `~` path). " +
|
|
747
|
+
(isRemoteExecutionEnv(env) ? "" : "A path that leads outside via a symlink counts as outside. ") +
|
|
748
|
+
"Still subject to the deployment's approval policy.",
|
|
670
749
|
parameters: Type.Object({
|
|
671
750
|
command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
|
|
672
751
|
timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${timeoutCaps.defaultMs}, max ${timeoutCaps.maxMs}; requests above the max are capped to it).` })),
|
|
@@ -678,8 +757,17 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opt
|
|
|
678
757
|
if (reason)
|
|
679
758
|
return errorResult(`Error (Bash): ${reason}`);
|
|
680
759
|
const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
|
|
681
|
-
if (boundary.reason !== undefined
|
|
682
|
-
|
|
760
|
+
if (boundary.reason !== undefined) {
|
|
761
|
+
const rescued = boundary.outOfRootRead === true && (await canonicalBoundary.allResolveInside(boundary.outOfRootPaths ?? [], ctx.signal));
|
|
762
|
+
if (!rescued && !(await readsOnlyEngineOverflowSpool(boundary))) {
|
|
763
|
+
return errorResult(`Error (Bash): ${boundary.reason}. bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.`, { code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const escaping = await canonicalBoundary.escapesAfterResolution(boundary.checkedPaths ?? [], ctx.signal);
|
|
767
|
+
if (escaping.length > 0) {
|
|
768
|
+
const quoted = escaping.map((p) => `"${p}"`).join(", ");
|
|
769
|
+
return errorResult(`Error (Bash): a path this command reads resolves through a symlink to ${quoted}, outside the allowed directories for this session: ${readRoots.join(", ")} — not auto-allowed. ` +
|
|
770
|
+
"bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.", { code: "readonly_out_of_root", paths: escaping });
|
|
683
771
|
}
|
|
684
772
|
return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
|
|
685
773
|
},
|
package/dist/tools/loop-tick.js
CHANGED
|
@@ -49,7 +49,7 @@ Read and analyze freely — understanding the state of things has no blast radiu
|
|
|
49
49
|
`;
|
|
50
50
|
const DYNAMIC_APPENDIX = `
|
|
51
51
|
|
|
52
|
-
If a Monitor is armed (
|
|
52
|
+
If a Monitor is armed (its task_id was reported in the receipt when you started it), keep \`delaySeconds\` at 1200–1800s — the Monitor is the wake signal and this is only the fallback heartbeat. If you were woken by a \`<task-notification>\`, handle the event before deciding whether to re-arm. To stop the loop, call ${SCHEDULE_WAKEUP_TOOL_NAME} with \`stop: true\` and TaskStop the monitor (its task id is in that start receipt, and in the \`<task-id>\` of every notification the watch sends).`;
|
|
53
53
|
function pushNotificationAppendix(persistentVariant) {
|
|
54
54
|
const r = persistentVariant
|
|
55
55
|
? "newly blocked on a decision you won't make alone, you're ending the loop"
|