@sema-agent/core 2.13.0 → 4.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -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();
@@ -1,4 +1,4 @@
1
- import { type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
1
+ import { type MailboxAppendMessage, type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
2
2
  export interface FileMailboxStoreOptions {
3
3
  fsync?: boolean;
4
4
  compactEvery?: number;
@@ -15,11 +15,7 @@ export declare class FileMailboxStore implements MailboxStore {
15
15
  private load;
16
16
  private commit;
17
17
  private compact;
18
- append(scope: string, handle: string, msg: {
19
- from?: string;
20
- content: string;
21
- sentAt: number;
22
- }): Promise<number>;
18
+ append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
23
19
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
24
20
  ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
25
21
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
@@ -105,11 +105,25 @@ export class FileSessionPolicyStore {
105
105
  for (const file of files) {
106
106
  if (!file.endsWith(".json"))
107
107
  continue;
108
+ let raw;
109
+ try {
110
+ raw = readFileSync(join(this.dir, file), "utf8");
111
+ }
112
+ catch {
113
+ continue;
114
+ }
108
115
  let parsed;
109
116
  try {
110
- parsed = JSON.parse(readFileSync(join(this.dir, file), "utf8"));
117
+ parsed = JSON.parse(raw);
111
118
  }
112
119
  catch {
120
+ try {
121
+ unlinkSync(join(this.dir, file));
122
+ }
123
+ catch (err) {
124
+ if (err.code !== "ENOENT")
125
+ throw err;
126
+ }
113
127
  continue;
114
128
  }
115
129
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
@@ -1,22 +1,20 @@
1
- import type { Session, SessionMetadata, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
1
+ import type { Session, SessionForkOptions, SessionMetadata, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
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>;
12
14
  open(metadata: SessionMetadata): Promise<Session>;
13
15
  list(): Promise<SessionMetadata[]>;
14
16
  delete(metadata: SessionMetadata): Promise<void>;
15
- fork(sourceMetadata: SessionMetadata, options?: {
16
- entryId?: string;
17
- position?: "before" | "at";
18
- id?: string;
19
- }): Promise<Session>;
17
+ fork(sourceMetadata: SessionMetadata, options?: SessionForkOptions): Promise<Session>;
20
18
  exportEntries(sessionId: string): Promise<SessionTreeEntry[]>;
21
19
  importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[]): Promise<void>;
22
20
  }
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { type WorkflowJournalEntry, type WorkflowJournalStore } from "../../core/workflow-journal-store.js";
1
+ import { type WorkflowJournalEntry, type WorkflowJournalStore, type ResumeClaimArgs } from "../../core/workflow-journal-store.js";
2
2
  export { MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "../../core/workflow-journal-store.js";
3
3
  export declare const RESUME_CLAIM_TTL_MS: number;
4
4
  export declare const RESUME_CLAIM_RELEASE_GRACE_MS = 5000;
@@ -24,19 +24,11 @@ export declare class FileWorkflowJournalStore implements WorkflowJournalStore {
24
24
  private readClaimRaw;
25
25
  private unlinkIfUnchanged;
26
26
  private sweepExpiredClaims;
27
- resumeClaim(input: {
28
- sourceRunId: string;
29
- newRunId: string;
30
- scope: string;
31
- }): Promise<{
27
+ resumeClaim(input: ResumeClaimArgs): Promise<{
32
28
  granted: boolean;
33
29
  holder?: string;
34
30
  }>;
35
- releaseResumeClaim(input: {
36
- sourceRunId: string;
37
- newRunId: string;
38
- scope: string;
39
- }): Promise<void>;
31
+ releaseResumeClaim(input: ResumeClaimArgs): Promise<void>;
40
32
  deleteByRun(runId: string): Promise<number>;
41
33
  dispose(): void;
42
34
  }
@@ -1,5 +1,5 @@
1
- import { type WorkflowRunStore, type WorkflowRunSummary } from "../../core/workflow-run-store.js";
2
- import type { WorkflowRun, WorkflowRunStatus } from "../../orchestration/workflow.js";
1
+ import { type WorkflowRunListByScopeOptions, type WorkflowRunStore, type WorkflowRunSummary } from "../../core/workflow-run-store.js";
2
+ import type { WorkflowRun } from "../../orchestration/workflow.js";
3
3
  export interface FileWorkflowRunStoreOptions {
4
4
  fsync?: boolean;
5
5
  compactEvery?: number;
@@ -18,11 +18,7 @@ export declare class FileWorkflowRunStore implements WorkflowRunStore {
18
18
  update(id: string, scope: string, run: WorkflowRun, expect?: {
19
19
  rev: number;
20
20
  }): Promise<boolean>;
21
- listByScope(scope: string, opts?: {
22
- status?: WorkflowRunStatus;
23
- limit?: number;
24
- session?: string;
25
- }): Promise<WorkflowRunSummary[]>;
21
+ listByScope(scope: string, opts?: WorkflowRunListByScopeOptions): Promise<WorkflowRunSummary[]>;
26
22
  reap(scope: string, now: number, opts?: {
27
23
  maxAgeMs?: number;
28
24
  keep?: number;
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { WorkflowRunStoreError, isTerminalWorkflowStatus, summarizeWorkflowRun, } from "../../core/workflow-run-store.js";
2
+ import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdate, queryWorkflowRuns, } from "../../core/workflow-run-store.js";
3
3
  import { SharedLedgerTable } from "./shared-ledger.js";
4
4
  const runLedgers = new SharedLedgerTable({
5
5
  keyOf: (run) => run.id,
@@ -53,28 +53,15 @@ export class FileWorkflowRunStore {
53
53
  }
54
54
  async update(id, scope, run, expect) {
55
55
  return this.withLock(id, () => {
56
- const cur = this.runs.get(id);
57
- if (!cur || cur.scope !== scope) {
56
+ const next = nextWorkflowRunOnUpdate(this.runs.get(id), id, scope, run, expect);
57
+ if (!next)
58
58
  return false;
59
- }
60
- if (expect !== undefined && (cur.rev ?? 0) !== expect.rev) {
61
- return false;
62
- }
63
- const next = structuredClone(run);
64
- next.id = id;
65
- next.scope = scope;
66
- next.rev = (cur.rev ?? 0) + 1;
67
59
  this.commit({ t: "update", id, run: next }, this.fsyncEnabled);
68
60
  return true;
69
61
  });
70
62
  }
71
63
  async listByScope(scope, opts) {
72
- const rows = [...this.runs.values()].filter((r) => r.scope === scope &&
73
- (opts?.status === undefined || r.status === opts.status) &&
74
- (opts?.session === undefined || r.originatingSessionId === opts.session));
75
- rows.sort((a, b) => b.createdAt - a.createdAt);
76
- const limited = opts?.limit !== undefined ? rows.slice(0, Math.max(0, opts.limit)) : rows;
77
- return limited.map(summarizeWorkflowRun);
64
+ return queryWorkflowRuns(this.runs.values(), scope, opts);
78
65
  }
79
66
  async reap(scope, now, opts) {
80
67
  if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
@@ -14,6 +14,8 @@ export interface CompoundReadonlyVerdict {
14
14
  reason?: string;
15
15
  outOfRootRead?: true;
16
16
  outOfRootPaths?: readonly string[];
17
+ checkedPaths?: readonly string[];
18
+ undecidedPaths?: readonly string[];
17
19
  }
18
20
  export declare function formatOutOfRootReadApprovalOption(directory: string): string;
19
21
  export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
@@ -1,4 +1,4 @@
1
- import { isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
1
+ import { isAbsolutePathForm, isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
2
2
  export const BASH_READONLY_DEFAULT_ALLOW = [
3
3
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
4
4
  "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
@@ -56,31 +56,24 @@ export function formatOutOfRootReadApprovalOption(directory) {
56
56
  return `Yes, allow reading from ${leaf || directory}/ from this project`;
57
57
  }
58
58
  const NO_PATH_OPERAND_COMMANDS = new Set(["pwd", "echo", "whoami", "uname", "which", "tr", "basename", "dirname"]);
59
- function isAbsolutePathToken(p) {
60
- return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
61
- }
62
59
  function isPathShapedToken(tok) {
63
60
  return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
64
61
  }
65
- function normalizeAbsPathLexicalEitherFamily(p) {
66
- const s = p.replace(/\\/g, "/");
67
- return /^[A-Za-z]:\//.test(s) ? s.slice(0, 2) + normalizeAbsPathLexically(s.slice(2)) : normalizeAbsPathLexically(s);
68
- }
69
62
  function resolveOperandLexically(base, operand, homeDir) {
70
63
  let raw = operand;
71
64
  if (raw === "~" || raw.startsWith("~/")) {
72
- if (homeDir === undefined || !isAbsolutePathToken(homeDir))
65
+ if (homeDir === undefined || !isAbsolutePathForm(homeDir))
73
66
  return undefined;
74
67
  raw = raw === "~" ? homeDir : `${homeDir.replace(/[/\\]+$/, "")}/${raw.slice(2)}`;
75
68
  }
76
69
  else if (raw.startsWith("~")) {
77
70
  return undefined;
78
71
  }
79
- if (isAbsolutePathToken(raw))
80
- return normalizeAbsPathLexicalEitherFamily(raw);
81
- if (base === undefined || !isAbsolutePathToken(base))
72
+ if (isAbsolutePathForm(raw))
73
+ return normalizeAbsPathLexically(raw);
74
+ if (base === undefined || !isAbsolutePathForm(base))
82
75
  return undefined;
83
- return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
76
+ return normalizeAbsPathLexically(`${base.replace(/[/\\]+$/, "")}/${raw}`);
84
77
  }
85
78
  function tokenizeSegment(segment) {
86
79
  const raw = segment
@@ -107,6 +100,22 @@ function hasUnquotedExpansionMetachar(rawToken) {
107
100
  }
108
101
  return false;
109
102
  }
103
+ function hasUnquotedGlobMetachar(rawToken) {
104
+ let open;
105
+ for (const ch of rawToken) {
106
+ if (open === undefined && (ch === '"' || ch === "'")) {
107
+ open = ch;
108
+ continue;
109
+ }
110
+ if (open === ch) {
111
+ open = undefined;
112
+ continue;
113
+ }
114
+ if (open === undefined && (ch === "*" || ch === "?" || ch === "["))
115
+ return true;
116
+ }
117
+ return false;
118
+ }
110
119
  function longOptionNameOf(tok) {
111
120
  if (!tok.startsWith("--") || tok.length === 2)
112
121
  return undefined;
@@ -179,6 +188,11 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
179
188
  const args = tokens.folded.slice(1);
180
189
  const findings = [];
181
190
  const candidates = [];
191
+ const bareWordOperands = [];
192
+ const argGlobs = (k) => {
193
+ const raw = tokens.raw[k + 1];
194
+ return raw !== undefined && hasUnquotedGlobMetachar(raw);
195
+ };
182
196
  for (const rawArg of tokens.raw.slice(1)) {
183
197
  if (!hasUnquotedExpansionMetachar(rawArg))
184
198
  continue;
@@ -190,14 +204,18 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
190
204
  ];
191
205
  }
192
206
  if (name === "cd") {
193
- const target = args.find((t) => !t.startsWith("-") || t === "-");
207
+ const targetIdx = args.findIndex((t) => !t.startsWith("-") || t === "-");
208
+ const target = targetIdx === -1 ? undefined : args[targetIdx];
209
+ if (target !== undefined && argGlobs(targetIdx)) {
210
+ return [{ kind: "unresolvable", reason: `\`cd\` is given the pattern "${target}", which the shell expands to a directory this check cannot know — not auto-allowed` }];
211
+ }
194
212
  if (target === undefined) {
195
213
  return [{ kind: "unresolvable", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories — not auto-allowed' }];
196
214
  }
197
215
  if (target === "-") {
198
216
  return [{ kind: "unresolvable", reason: '`cd -` targets the previous working directory, which cannot be resolved statically — not auto-allowed' }];
199
217
  }
200
- candidates.push(target);
218
+ candidates.push({ text: target, globbed: false });
201
219
  }
202
220
  else {
203
221
  const patternSuppliedByFlag = name === "grep" && args.some(isGrepPatternFlagToken);
@@ -214,8 +232,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
214
232
  continue;
215
233
  if (t.startsWith("-") && t !== "-") {
216
234
  for (const payload of attachedOptionPayloads(t)) {
217
- if (isAbsolutePathToken(payload) || isPathShapedToken(payload))
218
- candidates.push(payload);
235
+ if (isAbsolutePathForm(payload) || isPathShapedToken(payload))
236
+ candidates.push({ text: payload, globbed: argGlobs(k) });
219
237
  }
220
238
  continue;
221
239
  }
@@ -226,10 +244,18 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
226
244
  if (isGrepPatternSlot)
227
245
  continue;
228
246
  if (isPathShapedToken(t))
229
- candidates.push(t);
247
+ candidates.push({ text: t, globbed: argGlobs(k) });
248
+ else
249
+ bareWordOperands.push({ text: t, globbed: argGlobs(k) });
230
250
  }
231
251
  }
232
- for (const candidate of candidates) {
252
+ for (let k = 0; k < args.length; k++) {
253
+ if (!argGlobs(k))
254
+ continue;
255
+ const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
256
+ findings.push({ kind: "undecided", path: resolved ?? args[k] });
257
+ }
258
+ for (const { text: candidate, globbed } of candidates) {
233
259
  const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], candidate, boundary.homeDir);
234
260
  if (resolved === undefined) {
235
261
  findings.push({
@@ -238,9 +264,20 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
238
264
  });
239
265
  continue;
240
266
  }
241
- if (!withinAnyRoot(boundary.roots, resolved))
267
+ if (withinAnyRoot(boundary.roots, resolved)) {
268
+ if (!globbed)
269
+ findings.push({ kind: "inside", path: resolved });
270
+ }
271
+ else
242
272
  findings.push({ kind: "outside", command: name, path: resolved });
243
273
  }
274
+ for (const { text: operand, globbed } of bareWordOperands) {
275
+ const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
276
+ if (resolved === undefined)
277
+ continue;
278
+ if (!globbed && withinAnyRoot(boundary.roots, resolved))
279
+ findings.push({ kind: "inside", path: resolved });
280
+ }
244
281
  return findings;
245
282
  }
246
283
  export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
@@ -383,22 +420,37 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
383
420
  }
384
421
  function evaluateReadBoundary(foldedSegments, boundary) {
385
422
  const outside = [];
423
+ const inside = [];
424
+ const undecided = [];
386
425
  for (const toks of foldedSegments) {
387
426
  for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
388
427
  if (finding.kind === "unresolvable")
389
428
  return { reason: finding.reason };
429
+ if (finding.kind === "inside") {
430
+ if (!inside.includes(finding.path))
431
+ inside.push(finding.path);
432
+ continue;
433
+ }
434
+ if (finding.kind === "undecided") {
435
+ if (!undecided.includes(finding.path))
436
+ undecided.push(finding.path);
437
+ continue;
438
+ }
390
439
  if (!outside.some((o) => o.path === finding.path))
391
440
  outside.push(finding);
392
441
  }
393
442
  }
443
+ const undecidedField = undecided.length > 0 ? { undecidedPaths: undecided } : {};
394
444
  if (outside.length === 0)
395
- return {};
445
+ return inside.length > 0 ? { checkedPaths: inside, ...undecidedField } : { ...undecidedField };
396
446
  const paths = outside.map((o) => `"${o.path}"`).join(", ");
397
447
  const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
398
448
  return {
399
449
  reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
400
450
  outOfRootRead: true,
401
451
  outOfRootPaths: outside.map((o) => o.path),
452
+ ...(inside.length > 0 ? { checkedPaths: inside } : {}),
453
+ ...undecidedField,
402
454
  };
403
455
  }
404
456
  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;