@namzu/sdk 59.0.0 → 60.1.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 +120 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +16 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +12 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/compaction/context-window.d.ts +53 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +141 -2
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/constants/continuation.d.ts +9 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +9 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/contracts/session/api.d.ts +7 -1
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/execution/workflow/index.d.ts +3 -1
- package/dist/execution/workflow/index.d.ts.map +1 -1
- package/dist/execution/workflow/index.js +2 -1
- package/dist/execution/workflow/index.js.map +1 -1
- package/dist/execution/workflow/journal.d.ts +24 -4
- package/dist/execution/workflow/journal.d.ts.map +1 -1
- package/dist/execution/workflow/journal.js +131 -9
- package/dist/execution/workflow/journal.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts +2 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +49 -8
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/resume.d.ts +59 -0
- package/dist/execution/workflow/resume.d.ts.map +1 -0
- package/dist/execution/workflow/resume.js +273 -0
- package/dist/execution/workflow/resume.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +112 -8
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +105 -1
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +1 -0
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/compat.d.ts +24 -0
- package/dist/plugin/compat.d.ts.map +1 -0
- package/dist/plugin/compat.js +149 -0
- package/dist/plugin/compat.js.map +1 -0
- package/dist/plugin/loader.d.ts +2 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +6 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/pricing/index.d.ts.map +1 -1
- package/dist/pricing/index.js +2 -1
- package/dist/pricing/index.js.map +1 -1
- package/dist/process/kill-tree.d.ts +44 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +102 -4
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +9 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/collect-chat-completion.d.ts.map +1 -1
- package/dist/provider/collect-chat-completion.js +4 -0
- package/dist/provider/collect-chat-completion.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +11 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/mock.d.ts +2 -0
- package/dist/provider/mock.d.ts.map +1 -1
- package/dist/provider/mock.js +16 -8
- package/dist/provider/mock.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts +18 -0
- package/dist/provider/model-window-suffix.d.ts.map +1 -0
- package/dist/provider/model-window-suffix.js +78 -0
- package/dist/provider/model-window-suffix.js.map +1 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +1 -4
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +0 -28
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +11 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
- package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
- package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
- package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
- package/dist/runtime/jobs/liveness.d.ts +67 -0
- package/dist/runtime/jobs/liveness.d.ts.map +1 -0
- package/dist/runtime/jobs/liveness.js +94 -0
- package/dist/runtime/jobs/liveness.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +104 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/jobs/wake-budget.d.ts +35 -0
- package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
- package/dist/runtime/jobs/wake-budget.js +48 -0
- package/dist/runtime/jobs/wake-budget.js.map +1 -0
- package/dist/runtime/query/detached-calls.d.ts +65 -0
- package/dist/runtime/query/detached-calls.d.ts.map +1 -0
- package/dist/runtime/query/detached-calls.js +116 -0
- package/dist/runtime/query/detached-calls.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +4 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +90 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +34 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +32 -5
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
- package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
- package/dist/runtime/query/iteration/effort-clamp.js +42 -0
- package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +34 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +199 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +11 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +6 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +8 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +29 -3
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +58 -3
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/system-events.d.ts +5 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +7 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/denial.d.ts +11 -0
- package/dist/sandbox/denial.d.ts.map +1 -0
- package/dist/sandbox/denial.js +61 -0
- package/dist/sandbox/denial.js.map +1 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +31 -13
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts +2 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +64 -13
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/ignore-matcher.d.ts +30 -0
- package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
- package/dist/sandbox/ignore-matcher.js +11 -0
- package/dist/sandbox/ignore-matcher.js.map +1 -0
- package/dist/sandbox/ignore-source.d.ts +26 -0
- package/dist/sandbox/ignore-source.d.ts.map +1 -0
- package/dist/sandbox/ignore-source.js +221 -0
- package/dist/sandbox/ignore-source.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +11 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +1 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/log-hash.d.ts.map +1 -1
- package/dist/session/log-hash.js +18 -1
- package/dist/session/log-hash.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +1 -1
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +1 -1
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/operation-lock.d.ts +20 -4
- package/dist/store/memory/operation-lock.d.ts.map +1 -1
- package/dist/store/memory/operation-lock.js +235 -58
- package/dist/store/memory/operation-lock.js.map +1 -1
- package/dist/store/session-index/sqlite.d.ts.map +1 -1
- package/dist/store/session-index/sqlite.js +169 -16
- package/dist/store/session-index/sqlite.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +10 -4
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +35 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +512 -67
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +65 -28
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +94 -40
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +34 -7
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +44 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -0
- package/dist/tools/builtins/search-support.js +89 -0
- package/dist/tools/builtins/search-support.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +13 -2
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +14 -5
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +11 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/computer-use/index.d.ts +129 -13
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +24 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +4 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +19 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +7 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +15 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +38 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/session/events.d.ts +34 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +298 -34
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +35 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +11 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +8 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/bridge/a2a/mapper.ts +19 -0
- package/src/bridge/acp/update.ts +6 -1
- package/src/bridge/sse/mapper.ts +13 -0
- package/src/compaction/context-window.ts +163 -2
- package/src/constants/continuation.ts +13 -0
- package/src/contracts/session/api.ts +6 -0
- package/src/execution/workflow/index.ts +14 -1
- package/src/execution/workflow/journal.ts +161 -11
- package/src/execution/workflow/prelude.ts +51 -8
- package/src/execution/workflow/resume.ts +313 -0
- package/src/execution/workflow/runtime.ts +127 -9
- package/src/execution/workflow/types.ts +109 -1
- package/src/index.ts +17 -0
- package/src/manager/session/turn-recorder.ts +1 -0
- package/src/plugin/compat.ts +173 -0
- package/src/plugin/loader.ts +7 -1
- package/src/pricing/index.ts +2 -1
- package/src/process/kill-tree.ts +106 -4
- package/src/prompt/coding-agent-doctrine.ts +9 -1
- package/src/provider/collect-chat-completion.ts +3 -0
- package/src/provider/fallback.ts +17 -0
- package/src/provider/idle-timeout.ts +9 -0
- package/src/provider/mock.ts +19 -9
- package/src/provider/model-window-suffix.ts +85 -0
- package/src/provider/retry.ts +9 -0
- package/src/public-runtime.ts +21 -2
- package/src/public-types.ts +1 -0
- package/src/registry/command/kernel-commands.ts +1 -32
- package/src/runtime/jobs/awaited-jobs.ts +11 -0
- package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
- package/src/runtime/jobs/liveness.ts +144 -0
- package/src/runtime/jobs/registry.ts +114 -2
- package/src/runtime/jobs/wake-budget.ts +55 -0
- package/src/runtime/query/detached-calls.ts +135 -0
- package/src/runtime/query/executor.ts +92 -10
- package/src/runtime/query/index.ts +65 -7
- package/src/runtime/query/iteration/effort-clamp.ts +46 -0
- package/src/runtime/query/iteration/index.ts +260 -6
- package/src/runtime/query/iteration/outstanding-work.ts +12 -3
- package/src/runtime/query/iteration/phases/context.ts +9 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
- package/src/runtime/query/iteration/stream-turn.ts +5 -0
- package/src/runtime/query/session-storage.ts +8 -3
- package/src/runtime/query/steering.ts +77 -6
- package/src/runtime/query/tooling.ts +2 -0
- package/src/runtime/system-events.ts +12 -1
- package/src/sandbox/denial.ts +79 -0
- package/src/sandbox/file-walk-program.ts +32 -13
- package/src/sandbox/file-walk.ts +75 -12
- package/src/sandbox/ignore-matcher.ts +46 -0
- package/src/sandbox/ignore-source.ts +220 -0
- package/src/sandbox/provider/local.ts +11 -1
- package/src/scheduler/completion-inbox.ts +1 -0
- package/src/session/log-hash.ts +21 -0
- package/src/store/memory/disk.ts +1 -0
- package/src/store/memory/markdown.ts +1 -0
- package/src/store/memory/operation-lock.ts +288 -57
- package/src/store/session-index/sqlite.ts +171 -16
- package/src/tools/builtins/bash.ts +11 -4
- package/src/tools/builtins/computer-use.ts +593 -68
- package/src/tools/builtins/glob.ts +79 -32
- package/src/tools/builtins/grep.ts +107 -43
- package/src/tools/builtins/job.ts +42 -12
- package/src/tools/builtins/search-support.ts +107 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
- package/src/tools/builtins/wait-for-job.ts +14 -2
- package/src/tools/command-shell.ts +13 -5
- package/src/turn/reporter.ts +12 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/computer-use/index.ts +157 -15
- package/src/types/message/index.ts +5 -3
- package/src/types/plugin/index.ts +8 -0
- package/src/types/provider/chat.ts +20 -0
- package/src/types/provider/config.ts +7 -0
- package/src/types/provider/index.ts +1 -0
- package/src/types/provider/interface.ts +20 -0
- package/src/types/provider/stream.ts +6 -0
- package/src/types/sandbox/index.ts +40 -1
- package/src/types/session/events.ts +65 -5
- package/src/types/session/records.ts +39 -2
- package/src/types/session/stop-reason.ts +11 -0
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +8 -0
|
@@ -64,17 +64,31 @@ export interface SteeringChannel {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export class SteeringBinding implements SteeringChannel {
|
|
67
|
-
private queued: string[] = []
|
|
67
|
+
private queued: { readonly text: string; readonly key?: string }[] = []
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* `key` names what the text is about (a job id), so the text can be
|
|
71
|
+
* taken back with {@link withdraw} while it is still undelivered.
|
|
72
|
+
*/
|
|
73
|
+
steer(text: string, key?: string): void {
|
|
70
74
|
const trimmed = text.trim()
|
|
71
75
|
if (!trimmed) return
|
|
72
|
-
this.queued.push(trimmed)
|
|
76
|
+
this.queued.push(key === undefined ? { text: trimmed } : { text: trimmed, key })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Take back every undelivered text queued under `key`. Returns whether any
|
|
81
|
+
* was queued. Text already drained has been delivered and is not touched.
|
|
82
|
+
*/
|
|
83
|
+
withdraw(key: string): boolean {
|
|
84
|
+
const before = this.queued.length
|
|
85
|
+
this.queued = this.queued.filter((entry) => entry.key !== key)
|
|
86
|
+
return this.queued.length !== before
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
drain(): string | undefined {
|
|
76
90
|
if (this.queued.length === 0) return undefined
|
|
77
|
-
const joined = this.queued.join('\n')
|
|
91
|
+
const joined = this.queued.map((entry) => entry.text).join('\n')
|
|
78
92
|
this.queued = []
|
|
79
93
|
return joined
|
|
80
94
|
}
|
|
@@ -84,6 +98,60 @@ export class SteeringBinding implements SteeringChannel {
|
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The most output a job-exit notice carries inline. A job whose whole output
|
|
103
|
+
* fits is told with its result; a larger one is read with the job tool, so a
|
|
104
|
+
* noisy build cannot turn every exit into a large message.
|
|
105
|
+
*/
|
|
106
|
+
export const JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES = 2048
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One job's exit as the text the model reads. The output is inlined only when
|
|
110
|
+
* the whole of it fits {@link JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES} and none was
|
|
111
|
+
* dropped by the retention cap; it is untrusted process output, so it rides in
|
|
112
|
+
* the system-event envelope rather than in the notice's own words.
|
|
113
|
+
*/
|
|
114
|
+
export function formatJobExitNotice(
|
|
115
|
+
job: Pick<BackgroundJob, 'id' | 'command' | 'status' | 'exitCode' | 'signal'> &
|
|
116
|
+
Partial<Pick<BackgroundJob, 'exitedAt'>>,
|
|
117
|
+
output?: {
|
|
118
|
+
readonly chunk: string
|
|
119
|
+
readonly nextOffset: number
|
|
120
|
+
readonly droppedBytes: number
|
|
121
|
+
},
|
|
122
|
+
): string {
|
|
123
|
+
const outcome =
|
|
124
|
+
job.status === 'killed'
|
|
125
|
+
? 'was stopped'
|
|
126
|
+
: `exited with code ${job.exitCode ?? 'unknown'}${job.signal ? ` (${job.signal})` : ''}`
|
|
127
|
+
const endedAt = job.exitedAt === undefined ? '' : ` at ${new Date(job.exitedAt).toISOString()}`
|
|
128
|
+
const head = `Background job ${job.id} (${job.command}) ${outcome}${endedAt}.`
|
|
129
|
+
if (
|
|
130
|
+
output === undefined ||
|
|
131
|
+
output.chunk.length === 0 ||
|
|
132
|
+
output.droppedBytes > 0 ||
|
|
133
|
+
output.nextOffset > JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES
|
|
134
|
+
) {
|
|
135
|
+
return `${head} Read its output with the job tool if you need it.`
|
|
136
|
+
}
|
|
137
|
+
return `${head}\n${formatSystemEvent({
|
|
138
|
+
kind: 'job',
|
|
139
|
+
id: job.id,
|
|
140
|
+
status: job.status === 'killed' ? 'cancelled' : job.exitCode === 0 ? 'completed' : 'failed',
|
|
141
|
+
summary: `Output of ${job.id}`,
|
|
142
|
+
source: `job ${job.id}: ${oneMetadataLine(job.command)}`,
|
|
143
|
+
body: {
|
|
144
|
+
envelope: {
|
|
145
|
+
kind: 'job-output',
|
|
146
|
+
attributes: { job: job.id },
|
|
147
|
+
provenance:
|
|
148
|
+
'Everything the job printed. The command, or whatever it reads, wrote it; it is not the operator.',
|
|
149
|
+
},
|
|
150
|
+
content: output.chunk,
|
|
151
|
+
},
|
|
152
|
+
})}`
|
|
153
|
+
}
|
|
154
|
+
|
|
87
155
|
/** What the model reads when a background job it started has ended. */
|
|
88
156
|
export function formatJobNote(text: string): string {
|
|
89
157
|
return frameNotice('job', '[Background job update]', text)
|
|
@@ -116,7 +184,7 @@ const SYSTEM_EVENT_FRAME = /^<system-event-([A-Za-z0-9]+) [^\n]*\n[\s\S]*?\n<\/s
|
|
|
116
184
|
*/
|
|
117
185
|
export function formatMonitorEvent(
|
|
118
186
|
event: Pick<MonitorEvent, 'jobId' | 'description' | 'kind' | 'text'> &
|
|
119
|
-
Partial<Pick<MonitorEvent, 'transport'>>,
|
|
187
|
+
Partial<Pick<MonitorEvent, 'transport' | 'at'>>,
|
|
120
188
|
command: string,
|
|
121
189
|
): string {
|
|
122
190
|
return formatSystemEvent({
|
|
@@ -126,6 +194,7 @@ export function formatMonitorEvent(
|
|
|
126
194
|
summary: `Monitor event: "${oneMetadataLine(event.description)}"`,
|
|
127
195
|
source: `monitor ${event.jobId}: ${oneMetadataLine(command)}`,
|
|
128
196
|
more: `job {"action":"kill","id":"${event.jobId}"} stops it`,
|
|
197
|
+
...(event.at !== undefined ? { at: event.at } : {}),
|
|
129
198
|
body: {
|
|
130
199
|
envelope: {
|
|
131
200
|
kind: event.kind === 'output' ? 'monitor-output' : 'monitor-notice',
|
|
@@ -170,7 +239,8 @@ export function formatMonitorEnd(
|
|
|
170
239
|
| 'transport'
|
|
171
240
|
| 'closeCode'
|
|
172
241
|
| 'closeReason'
|
|
173
|
-
|
|
242
|
+
> &
|
|
243
|
+
Partial<Pick<BackgroundJob, 'exitedAt'>>,
|
|
174
244
|
stats: { readonly stdoutChars: number },
|
|
175
245
|
): string {
|
|
176
246
|
const end = describeMonitorEnd(job, stats)
|
|
@@ -181,6 +251,7 @@ export function formatMonitorEnd(
|
|
|
181
251
|
summary: end.summary,
|
|
182
252
|
source: `monitor ${job.id}: ${oneMetadataLine(job.command)}`,
|
|
183
253
|
more: `job {"action":"read","id":"${job.id}"} reads its output`,
|
|
254
|
+
...(job.exitedAt !== undefined ? { at: job.exitedAt } : {}),
|
|
184
255
|
})
|
|
185
256
|
}
|
|
186
257
|
|
|
@@ -46,6 +46,7 @@ export interface ToolingBootstrapConfig {
|
|
|
46
46
|
backgroundJobOwner?: string
|
|
47
47
|
/** Where `wait_for_job` records wait-intent; see the executor's own field. */
|
|
48
48
|
onJobAwaited?: (id: string) => void
|
|
49
|
+
onJobCollected?: (id: string) => void
|
|
49
50
|
/** See `ToolExecutorConfig.monitorTimeoutCapMs`. */
|
|
50
51
|
monitorTimeoutCapMs?: number
|
|
51
52
|
/** See `ToolExecutorConfig.onMonitorStarted`. */
|
|
@@ -110,6 +111,7 @@ export class ToolingBootstrap {
|
|
|
110
111
|
...(config.backgroundJobs ? { backgroundJobs: config.backgroundJobs } : {}),
|
|
111
112
|
...(config.backgroundJobOwner ? { backgroundJobOwner: config.backgroundJobOwner } : {}),
|
|
112
113
|
...(config.onJobAwaited ? { onJobAwaited: config.onJobAwaited } : {}),
|
|
114
|
+
...(config.onJobCollected ? { onJobCollected: config.onJobCollected } : {}),
|
|
113
115
|
...(config.monitorTimeoutCapMs !== undefined
|
|
114
116
|
? { monitorTimeoutCapMs: config.monitorTimeoutCapMs }
|
|
115
117
|
: {}),
|
|
@@ -102,6 +102,11 @@ export interface SystemEvent {
|
|
|
102
102
|
readonly more?: string
|
|
103
103
|
/** Omit for an event with nothing to show; renders as `(no output)`. */
|
|
104
104
|
readonly body?: SystemEventBody
|
|
105
|
+
/**
|
|
106
|
+
* When it happened (epoch ms). Rendered as an `at` attribute in ISO form so
|
|
107
|
+
* the model can tell a fresh event from one that waited in a queue.
|
|
108
|
+
*/
|
|
109
|
+
readonly at?: number
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
export interface FormatSystemEventOptions {
|
|
@@ -209,6 +214,12 @@ function renderBody(body: SystemEventBody | undefined, options: FormatSystemEven
|
|
|
209
214
|
)
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
function atAttribute(at: number | undefined): string {
|
|
218
|
+
if (at === undefined || !Number.isFinite(at)) return ''
|
|
219
|
+
const date = new Date(at)
|
|
220
|
+
return Number.isNaN(date.getTime()) ? '' : ` at="${date.toISOString()}"`
|
|
221
|
+
}
|
|
222
|
+
|
|
212
223
|
/**
|
|
213
224
|
* Render one asynchronous event as provider-required user-role context.
|
|
214
225
|
*
|
|
@@ -248,7 +259,7 @@ export function formatSystemEvent(
|
|
|
248
259
|
])
|
|
249
260
|
const closingTag = `</system-event-${nonce}>`
|
|
250
261
|
const lines = [
|
|
251
|
-
`<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}">`,
|
|
262
|
+
`<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}"${atAttribute(event.at)}>`,
|
|
252
263
|
SYSTEM_EVENT_HEADER,
|
|
253
264
|
nonceClosureStatement(closingTag),
|
|
254
265
|
`summary: ${summary}`,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { SandboxEnvironment, SandboxExecAttribution } from '../types/sandbox/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reading a sandboxed command's failure: did the command fail, did the
|
|
5
|
+
* sandbox refuse it, or did the launcher never get as far as running it?
|
|
6
|
+
*
|
|
7
|
+
* The model cannot tell these apart from an exit code. A denied write looks
|
|
8
|
+
* like any other failure, so it retries the same command and burns turns
|
|
9
|
+
* on something no retry can fix; a launcher that could not set up its
|
|
10
|
+
* namespaces looks like the command crashing. Both call for a different
|
|
11
|
+
* answer (ask for the escape, or report a runner fault).
|
|
12
|
+
*
|
|
13
|
+
* This is inference from stderr text, not a verdict from the kernel, and
|
|
14
|
+
* the wording says so. It only ever adds a note; the result is unchanged.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Launcher setup failures print this prefix before any user command runs. */
|
|
18
|
+
const RUNNER_PREFIX: Partial<Record<SandboxEnvironment, RegExp>> = {
|
|
19
|
+
'linux-bwrap': /^bwrap: (?!execvp )/m,
|
|
20
|
+
'linux-namespace': /^unshare: /m,
|
|
21
|
+
'macos-seatbelt': /^sandbox-exec: /m,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const EROFS = /Read-only file system|\bEROFS\b/i
|
|
25
|
+
const PERMISSION = /Permission denied|Operation not permitted|\bEACCES\b|\bEPERM\b/i
|
|
26
|
+
const ABSOLUTE_PATH = /(?:^|[\s'"`:(])(\/[^\s'"`:)]+)/g
|
|
27
|
+
|
|
28
|
+
/** Only tiers that confine file writes can produce a sandbox denial. */
|
|
29
|
+
const CONFINES_FILES: ReadonlySet<SandboxEnvironment> = new Set(['linux-bwrap', 'macos-seatbelt'])
|
|
30
|
+
|
|
31
|
+
function within(path: string, root: string): boolean {
|
|
32
|
+
return path === root || path.startsWith(root.endsWith('/') ? root : `${root}/`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pathsIn(line: string): string[] {
|
|
36
|
+
return [...line.matchAll(ABSOLUTE_PATH)].map((m) => m[1] as string)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AttributionInput {
|
|
40
|
+
readonly environment: SandboxEnvironment
|
|
41
|
+
readonly exitCode: number
|
|
42
|
+
readonly stderr: string
|
|
43
|
+
readonly timedOut: boolean
|
|
44
|
+
/** Paths the sandbox lets the command write: root, added directories, scratch. */
|
|
45
|
+
readonly allowedRoots: readonly string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function attributeSandboxFailure(
|
|
49
|
+
input: AttributionInput,
|
|
50
|
+
): SandboxExecAttribution | undefined {
|
|
51
|
+
if (input.exitCode === 0 || input.timedOut || input.stderr === '') return undefined
|
|
52
|
+
|
|
53
|
+
const runner = RUNNER_PREFIX[input.environment]
|
|
54
|
+
if (runner?.test(input.stderr)) {
|
|
55
|
+
const line = input.stderr.split('\n').find((l) => runner.test(l)) ?? ''
|
|
56
|
+
return {
|
|
57
|
+
kind: 'runner-failure',
|
|
58
|
+
message: `The sandbox launcher failed before the command could run (${line.trim()}). This is a runner fault, not a failure of the command; retrying the same command will not help.`,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!CONFINES_FILES.has(input.environment)) return undefined
|
|
63
|
+
|
|
64
|
+
for (const line of input.stderr.split('\n')) {
|
|
65
|
+
if (EROFS.test(line)) return denial()
|
|
66
|
+
if (!PERMISSION.test(line)) continue
|
|
67
|
+
const paths = pathsIn(line)
|
|
68
|
+
if (paths.some((p) => !input.allowedRoots.some((root) => within(p, root)))) return denial()
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function denial(): SandboxExecAttribution {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'denial',
|
|
76
|
+
message:
|
|
77
|
+
'This looks like a sandbox denial: the command touched a path the sandbox does not allow it to write. This is inferred from the error text, not proven. Retrying the same command will fail the same way; if the action is needed, ask to run it outside the sandbox instead.',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { IGNORE_SOURCE } from './ignore-source.js'
|
|
2
|
+
|
|
1
3
|
/** Stable guest code: host bundlers must never rewrite a function we serialize. */
|
|
2
|
-
export const FILE_WALK_PROGRAM = String.raw`
|
|
4
|
+
export const FILE_WALK_PROGRAM = `${IGNORE_SOURCE}${String.raw`
|
|
3
5
|
const fs = require('node:fs/promises');
|
|
4
6
|
const path = require('node:path');
|
|
5
7
|
const plan = JSON.parse(process.argv[1]);
|
|
@@ -9,10 +11,17 @@ const write = async value => {
|
|
|
9
11
|
await new Promise(resolve => process.stdout.once('drain', resolve));
|
|
10
12
|
}
|
|
11
13
|
};
|
|
14
|
+
let stopped;
|
|
15
|
+
let visitedTotal = 0;
|
|
16
|
+
let ignoredDirs = 0;
|
|
12
17
|
const visit = count => {
|
|
13
|
-
|
|
18
|
+
visitedTotal = count;
|
|
19
|
+
if (count <= plan.maxVisitedEntries) return true;
|
|
20
|
+
if (plan.failOnLimit) {
|
|
14
21
|
throw Object.assign(new Error('File search stopped after examining ' + plan.maxVisitedEntries + ' entries; narrow its root or pattern.'), {code:'ERR_FILE_WALK_LIMIT'});
|
|
15
22
|
}
|
|
23
|
+
stopped = 'entry-limit';
|
|
24
|
+
return false;
|
|
16
25
|
};
|
|
17
26
|
const patterns = plan.patterns.map(set => set.map(part =>
|
|
18
27
|
typeof part === 'object' && part !== null ? new RegExp(part.source, part.flags + 's') : part));
|
|
@@ -41,9 +50,13 @@ async function run() {
|
|
|
41
50
|
let emitted = 0;
|
|
42
51
|
const matchers = plan.expressions.map(({source, flags}) => new RegExp(source, flags + 's'));
|
|
43
52
|
const maxDepth = plan.maxDepth === null ? Infinity : plan.maxDepth;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
const ignore = plan.respectIgnore ? ignCreate({fs, path, extraFiles: []}) : null;
|
|
54
|
+
const open = async (absolute, relative, depth, layered) => {
|
|
55
|
+
try {
|
|
56
|
+
const dir = await fs.opendir(absolute);
|
|
57
|
+
if (layered && ignore) await ignore.enter(absolute);
|
|
58
|
+
stack.push({dir, relative, depth, layered});
|
|
59
|
+
} catch (error) { if (!missing(error)) throw error; }
|
|
47
60
|
};
|
|
48
61
|
try {
|
|
49
62
|
if (plan.patterns.length === 0) return;
|
|
@@ -53,36 +66,42 @@ async function run() {
|
|
|
53
66
|
if (canonicalRoot !== path.resolve(plan.root)) throw new Error('File walk root follows a symbolic link; use its authorized real directory');
|
|
54
67
|
const rootInfo = await fs.lstat(plan.root);
|
|
55
68
|
if (rootInfo.isFile()) {
|
|
56
|
-
visit(++visited);
|
|
69
|
+
if (!visit(++visited)) return;
|
|
57
70
|
if (matchers.some(matcher => matcher.test(path.basename(plan.root)))) {
|
|
58
71
|
await write({type:'entry', path:plan.root, size:rootInfo.size});
|
|
59
72
|
}
|
|
60
73
|
return;
|
|
61
74
|
}
|
|
75
|
+
if (ignore) { await ignore.init(plan.root); await ignore.enter(plan.root); }
|
|
62
76
|
let start = plan.root;
|
|
63
77
|
for (const part of plan.prefix) {
|
|
64
78
|
start = path.join(start, part);
|
|
65
79
|
let info;
|
|
66
80
|
try { info = await fs.lstat(start); }
|
|
67
81
|
catch (error) { if (missing(error)) return; throw error; }
|
|
68
|
-
visit(++visited);
|
|
82
|
+
if (!visit(++visited)) return;
|
|
69
83
|
if (!info.isDirectory()) return;
|
|
84
|
+
if (ignore) await ignore.enter(start);
|
|
70
85
|
}
|
|
71
86
|
if (plan.prefix.length >= maxDepth) return;
|
|
72
|
-
await open(start, plan.prefix.join('/'), plan.prefix.length);
|
|
87
|
+
await open(start, plan.prefix.join('/'), plan.prefix.length, false);
|
|
73
88
|
while (stack.length) {
|
|
74
89
|
const frame = stack[stack.length - 1];
|
|
75
90
|
const entry = await frame.dir.read();
|
|
76
|
-
if (!entry) { stack.pop(); await frame.dir.close(); continue; }
|
|
77
|
-
visit(++visited);
|
|
91
|
+
if (!entry) { stack.pop(); if (frame.layered && ignore) ignore.leave(); await frame.dir.close(); continue; }
|
|
92
|
+
if (!visit(++visited)) return;
|
|
93
|
+
if (IGN_VCS.includes(entry.name)) continue;
|
|
78
94
|
const relative = frame.relative ? frame.relative + '/' + entry.name : entry.name;
|
|
79
95
|
const absolute = path.join(plan.root, relative);
|
|
80
96
|
const depth = frame.depth + 1;
|
|
81
97
|
if (entry.isDirectory()) {
|
|
82
|
-
if (depth
|
|
98
|
+
if (depth >= maxDepth || !canDescend(relative)) continue;
|
|
99
|
+
if (ignore && ignore.isIgnored(absolute, true)) { ignoredDirs++; continue; }
|
|
100
|
+
await open(absolute, relative, depth, true);
|
|
83
101
|
continue;
|
|
84
102
|
}
|
|
85
103
|
if (!entry.isFile() || !matchers.some(matcher => matcher.test(relative))) continue;
|
|
104
|
+
if (ignore && ignore.isIgnored(absolute, false)) continue;
|
|
86
105
|
let info;
|
|
87
106
|
try { info = await fs.lstat(absolute); }
|
|
88
107
|
catch (error) { if (missing(error)) continue; throw error; }
|
|
@@ -95,7 +114,7 @@ async function run() {
|
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
116
|
run().then(
|
|
98
|
-
() => write({type:'done'}),
|
|
117
|
+
() => write({type:'done', visited:visitedTotal, ignoredDirs, stopped}),
|
|
99
118
|
async error => { await write({type:'error', message:error.message, code:error.code}); process.exitCode = 1; },
|
|
100
119
|
);
|
|
101
|
-
`
|
|
120
|
+
`}`
|
package/src/sandbox/file-walk.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAbsolute, posix } from 'node:path'
|
|
1
|
+
import { isAbsolute, join, posix } from 'node:path'
|
|
2
2
|
import braceExpansion from 'brace-expansion'
|
|
3
3
|
import { GLOBSTAR, Minimatch } from 'minimatch'
|
|
4
4
|
import type {
|
|
@@ -6,9 +6,14 @@ import type {
|
|
|
6
6
|
SandboxExecResult,
|
|
7
7
|
SandboxFileEntry,
|
|
8
8
|
SandboxWalkFilesOptions,
|
|
9
|
+
SandboxWalkSummary,
|
|
9
10
|
} from '../types/sandbox/index.js'
|
|
10
11
|
import { subscribeToAbort } from '../utils/abort.js'
|
|
11
12
|
import { FILE_WALK_PROGRAM } from './file-walk-program.js'
|
|
13
|
+
import { VCS_NAMES, createIgnoreStack } from './ignore-matcher.js'
|
|
14
|
+
|
|
15
|
+
/** Entries a walk examines before it stops with what it has: a safety valve, not a budget. */
|
|
16
|
+
export const DEFAULT_MAX_VISITED_ENTRIES = 500_000
|
|
12
17
|
|
|
13
18
|
interface WalkPlan {
|
|
14
19
|
root: string
|
|
@@ -18,6 +23,8 @@ interface WalkPlan {
|
|
|
18
23
|
maxEntries: number
|
|
19
24
|
maxVisitedEntries: number
|
|
20
25
|
includeHidden: boolean
|
|
26
|
+
respectIgnore: boolean
|
|
27
|
+
failOnLimit: boolean
|
|
21
28
|
patterns: (string | { source: string; flags: string } | null)[][]
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -103,8 +110,13 @@ function planWalk(root: string, options: SandboxWalkFilesOptions): WalkPlan {
|
|
|
103
110
|
expressions,
|
|
104
111
|
maxDepth: Number.isFinite(maxDepth) ? maxDepth : null,
|
|
105
112
|
maxEntries: positiveInteger(options.maxEntries, 'maxEntries'),
|
|
106
|
-
maxVisitedEntries: positiveInteger(
|
|
113
|
+
maxVisitedEntries: positiveInteger(
|
|
114
|
+
options.maxVisitedEntries ?? DEFAULT_MAX_VISITED_ENTRIES,
|
|
115
|
+
'maxVisitedEntries',
|
|
116
|
+
),
|
|
107
117
|
includeHidden: options.includeHidden ?? false,
|
|
118
|
+
respectIgnore: options.respectIgnore ?? true,
|
|
119
|
+
failOnLimit: options.failOnLimit ?? false,
|
|
108
120
|
patterns: sets.map((set) =>
|
|
109
121
|
set.map((segment) =>
|
|
110
122
|
segment === GLOBSTAR
|
|
@@ -151,13 +163,25 @@ function directoryMatcher(plan: WalkPlan): (relative: string) => boolean {
|
|
|
151
163
|
async function* executeWalk(
|
|
152
164
|
plan: WalkPlan,
|
|
153
165
|
signal?: AbortSignal,
|
|
166
|
+
onSummary?: (summary: SandboxWalkSummary) => void,
|
|
154
167
|
): AsyncGenerator<SandboxFileEntry> {
|
|
155
168
|
const fs = await import('node:fs/promises')
|
|
156
169
|
const path = await import('node:path')
|
|
157
170
|
const { EventEmitter } = await import('node:events')
|
|
158
|
-
const open: {
|
|
171
|
+
const open: {
|
|
172
|
+
dir: import('node:fs').Dir
|
|
173
|
+
relative: string
|
|
174
|
+
depth: number
|
|
175
|
+
/** Whether leaving this directory also drops its ignore layer. */
|
|
176
|
+
layered: boolean
|
|
177
|
+
}[] = []
|
|
159
178
|
let visited = 0
|
|
160
179
|
let emitted = 0
|
|
180
|
+
let ignoredDirectories = 0
|
|
181
|
+
let stopped: SandboxWalkSummary['stopped']
|
|
182
|
+
const ignore = plan.respectIgnore
|
|
183
|
+
? createIgnoreStack({ fs, path, extraFiles: globalIgnoreFiles() })
|
|
184
|
+
: undefined
|
|
161
185
|
const matchers = plan.expressions.map(({ source, flags }) => new RegExp(source, `${flags}s`))
|
|
162
186
|
const canDescend = directoryMatcher(plan)
|
|
163
187
|
const maxDepth = plan.maxDepth ?? Number.POSITIVE_INFINITY
|
|
@@ -204,9 +228,11 @@ async function* executeWalk(
|
|
|
204
228
|
dispose?.()
|
|
205
229
|
}
|
|
206
230
|
}
|
|
231
|
+
/** False once the safety valve trips; the walk then ends with what it has. */
|
|
207
232
|
const noteVisit = () => {
|
|
208
233
|
visited += 1
|
|
209
|
-
if (visited
|
|
234
|
+
if (visited <= plan.maxVisitedEntries) return true
|
|
235
|
+
if (plan.failOnLimit) {
|
|
210
236
|
throw Object.assign(
|
|
211
237
|
new Error(
|
|
212
238
|
`File search stopped after examining ${plan.maxVisitedEntries} entries; narrow its root or pattern.`,
|
|
@@ -214,6 +240,8 @@ async function* executeWalk(
|
|
|
214
240
|
{ code: 'ERR_FILE_WALK_LIMIT' },
|
|
215
241
|
)
|
|
216
242
|
}
|
|
243
|
+
stopped = 'entry-limit'
|
|
244
|
+
return false
|
|
217
245
|
}
|
|
218
246
|
try {
|
|
219
247
|
check()
|
|
@@ -231,11 +259,14 @@ async function* executeWalk(
|
|
|
231
259
|
const rootInfo = await wait(fs.lstat(plan.root))
|
|
232
260
|
check()
|
|
233
261
|
if (rootInfo.isFile()) {
|
|
234
|
-
noteVisit()
|
|
262
|
+
if (!noteVisit()) return
|
|
235
263
|
if (matchers.some((matcher) => matcher.test(path.basename(plan.root))))
|
|
236
264
|
yield { path: plan.root, size: rootInfo.size }
|
|
237
265
|
return
|
|
238
266
|
}
|
|
267
|
+
// An explicit root is never itself ignored: only what is below it is tested.
|
|
268
|
+
await ignore?.init(plan.root)
|
|
269
|
+
await ignore?.enter(plan.root)
|
|
239
270
|
let start = plan.root
|
|
240
271
|
// Never follow a static-prefix symlink to speed up a pattern: traversal
|
|
241
272
|
// and its optimized starting point must have the same link policy.
|
|
@@ -249,22 +280,29 @@ async function* executeWalk(
|
|
|
249
280
|
if (missing(error)) return
|
|
250
281
|
throw error
|
|
251
282
|
}
|
|
252
|
-
noteVisit()
|
|
283
|
+
if (!noteVisit()) return
|
|
253
284
|
if (!info.isDirectory()) return
|
|
285
|
+
await ignore?.enter(start)
|
|
254
286
|
}
|
|
255
287
|
if (plan.prefix.length >= maxDepth) return
|
|
256
|
-
const openDirectory = async (
|
|
288
|
+
const openDirectory = async (
|
|
289
|
+
absolute: string,
|
|
290
|
+
relative: string,
|
|
291
|
+
depth: number,
|
|
292
|
+
layered: boolean,
|
|
293
|
+
) => {
|
|
257
294
|
check()
|
|
258
295
|
try {
|
|
259
296
|
const dir = await wait(fs.opendir(absolute), (late) => {
|
|
260
297
|
void close(late).catch(() => {})
|
|
261
298
|
})
|
|
262
|
-
|
|
299
|
+
if (layered) await ignore?.enter(absolute)
|
|
300
|
+
open.push({ dir, relative, depth, layered })
|
|
263
301
|
} catch (error) {
|
|
264
302
|
if (!missing(error)) throw error
|
|
265
303
|
}
|
|
266
304
|
}
|
|
267
|
-
await openDirectory(start, plan.prefix.join('/'), plan.prefix.length)
|
|
305
|
+
await openDirectory(start, plan.prefix.join('/'), plan.prefix.length, false)
|
|
268
306
|
while (open.length > 0) {
|
|
269
307
|
check()
|
|
270
308
|
const frame = open[open.length - 1] as (typeof open)[number]
|
|
@@ -272,18 +310,26 @@ async function* executeWalk(
|
|
|
272
310
|
check()
|
|
273
311
|
if (!entry) {
|
|
274
312
|
open.pop()
|
|
313
|
+
if (frame.layered) ignore?.leave()
|
|
275
314
|
await close(frame.dir)
|
|
276
315
|
continue
|
|
277
316
|
}
|
|
278
|
-
noteVisit()
|
|
317
|
+
if (!noteVisit()) return
|
|
318
|
+
if (VCS_NAMES.has(entry.name)) continue
|
|
279
319
|
const relative = frame.relative ? `${frame.relative}/${entry.name}` : entry.name
|
|
280
320
|
const absolute = path.join(plan.root, relative)
|
|
281
321
|
const depth = frame.depth + 1
|
|
282
322
|
if (entry.isDirectory()) {
|
|
283
|
-
if (depth
|
|
323
|
+
if (depth >= maxDepth || !canDescend(relative)) continue
|
|
324
|
+
if (ignore?.isIgnored(absolute, true)) {
|
|
325
|
+
ignoredDirectories += 1
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
await openDirectory(absolute, relative, depth, true)
|
|
284
329
|
continue
|
|
285
330
|
}
|
|
286
331
|
if (!entry.isFile() || !matchers.some((matcher) => matcher.test(relative))) continue
|
|
332
|
+
if (ignore?.isIgnored(absolute, false)) continue
|
|
287
333
|
let info: import('node:fs').Stats
|
|
288
334
|
try {
|
|
289
335
|
info = await wait(fs.lstat(absolute))
|
|
@@ -303,16 +349,24 @@ async function* executeWalk(
|
|
|
303
349
|
void close(dir).catch(() => {})
|
|
304
350
|
} else await close(dir)
|
|
305
351
|
}
|
|
352
|
+
onSummary?.({ visited, ignoredDirectories, ...(stopped ? { stopped } : {}) })
|
|
306
353
|
}
|
|
307
354
|
}
|
|
308
355
|
|
|
356
|
+
/** Global exclude files, lowest precedence. Only the host has them. */
|
|
357
|
+
function globalIgnoreFiles(): string[] {
|
|
358
|
+
const home = process.env.HOME ?? process.env.USERPROFILE
|
|
359
|
+
const base = process.env.XDG_CONFIG_HOME || (home ? join(home, '.config') : undefined)
|
|
360
|
+
return base ? [join(base, 'git', 'ignore')] : []
|
|
361
|
+
}
|
|
362
|
+
|
|
309
363
|
/** Internal host path; the caller first resolves its authorized filesystem root. */
|
|
310
364
|
export async function* walkFilesLocally(
|
|
311
365
|
rootPath: string,
|
|
312
366
|
options: SandboxWalkFilesOptions,
|
|
313
367
|
): AsyncGenerator<SandboxFileEntry> {
|
|
314
368
|
options.signal?.throwIfAborted()
|
|
315
|
-
yield* executeWalk(planWalk(rootPath, options), options.signal)
|
|
369
|
+
yield* executeWalk(planWalk(rootPath, options), options.signal, options.onSummary)
|
|
316
370
|
}
|
|
317
371
|
|
|
318
372
|
/** The existing sandbox execution seam; no separate worker protocol is needed. */
|
|
@@ -346,6 +400,7 @@ export async function* walkFilesViaExec(
|
|
|
346
400
|
let terminal = false
|
|
347
401
|
let received = 0
|
|
348
402
|
let failure: unknown
|
|
403
|
+
const outcome: { summary?: SandboxWalkSummary } = {}
|
|
349
404
|
const reject = (error: unknown) => {
|
|
350
405
|
failure ??= error instanceof Error ? error : new Error(String(error))
|
|
351
406
|
controller.abort(error)
|
|
@@ -358,6 +413,13 @@ export async function* walkFilesViaExec(
|
|
|
358
413
|
const record = value as Record<string, unknown>
|
|
359
414
|
if (record.type === 'done') {
|
|
360
415
|
terminal = true
|
|
416
|
+
const count = (value: unknown) =>
|
|
417
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0
|
|
418
|
+
outcome.summary = {
|
|
419
|
+
visited: count(record.visited),
|
|
420
|
+
ignoredDirectories: count(record.ignoredDirs),
|
|
421
|
+
...(record.stopped === 'entry-limit' ? { stopped: 'entry-limit' as const } : {}),
|
|
422
|
+
}
|
|
361
423
|
return
|
|
362
424
|
}
|
|
363
425
|
if (record.type === 'error' && typeof record.message === 'string') {
|
|
@@ -462,6 +524,7 @@ export async function* walkFilesViaExec(
|
|
|
462
524
|
}
|
|
463
525
|
} finally {
|
|
464
526
|
dispose?.()
|
|
527
|
+
if (outcome.summary) options.onSummary?.(outcome.summary)
|
|
465
528
|
if (!settled) controller.abort(new Error('File walk consumer stopped'))
|
|
466
529
|
await pending
|
|
467
530
|
// A rejected cancellation can mean remote work is still alive. Preserve
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { IGNORE_SOURCE } from './ignore-source.js'
|
|
2
|
+
|
|
3
|
+
/** One compiled ignore rule. */
|
|
4
|
+
export interface IgnoreRule {
|
|
5
|
+
readonly negate: boolean
|
|
6
|
+
readonly dirOnly: boolean
|
|
7
|
+
readonly re: RegExp
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Layered ignore rules for one depth-first walk; see `ignore-source.ts`. */
|
|
11
|
+
export interface IgnoreStack {
|
|
12
|
+
/** Find the enclosing repository and load the layers above the walk root. */
|
|
13
|
+
init(root: string): Promise<void>
|
|
14
|
+
/** Load a directory's rule files; call before reading it. */
|
|
15
|
+
enter(directory: string): Promise<void>
|
|
16
|
+
/** Drop the layer of the directory most recently entered. */
|
|
17
|
+
leave(): void
|
|
18
|
+
isIgnored(absolute: string, isDirectory: boolean): boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IgnoreDeps {
|
|
22
|
+
readonly fs: unknown
|
|
23
|
+
readonly path: unknown
|
|
24
|
+
/** Global exclude files, lowest precedence. The guest has none. */
|
|
25
|
+
readonly extraFiles?: readonly string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface IgnoreModule {
|
|
29
|
+
parse(text: string): IgnoreRule[]
|
|
30
|
+
decide(rules: readonly IgnoreRule[], relative: string, isDirectory: boolean): 0 | 1 | 2
|
|
31
|
+
create(deps: IgnoreDeps): IgnoreStack
|
|
32
|
+
vcs: readonly string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The same text is embedded in the guest program, so both walks share it.
|
|
36
|
+
const module_ = new Function(
|
|
37
|
+
`${IGNORE_SOURCE}\nreturn { parse: ignParse, decide: ignDecide, create: ignCreate, vcs: IGN_VCS }`,
|
|
38
|
+
)() as IgnoreModule
|
|
39
|
+
|
|
40
|
+
/** Parse the text of one ignore file into rules; a line it cannot read is dropped. */
|
|
41
|
+
export const parseIgnoreRules = module_.parse
|
|
42
|
+
/** 0: no rule matches, 1: ignored, 2: re-included. The last matching rule wins. */
|
|
43
|
+
export const decideIgnore = module_.decide
|
|
44
|
+
export const createIgnoreStack = module_.create
|
|
45
|
+
/** Version-control directories that no walk ever descends into. */
|
|
46
|
+
export const VCS_NAMES: ReadonlySet<string> = new Set(module_.vcs)
|