@namzu/sdk 32.0.0 → 33.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/CHANGELOG.md +127 -0
- package/dist/authorization/gate.d.ts +1 -1
- package/dist/authorization/gate.js +1 -1
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +71 -16
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +3 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/http-redirect.d.ts +17 -0
- package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
- package/dist/connector/mcp/http-redirect.js +31 -0
- package/dist/connector/mcp/http-redirect.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +5 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/image-admission.d.ts +16 -0
- package/dist/connector/mcp/image-admission.d.ts.map +1 -0
- package/dist/connector/mcp/image-admission.js +288 -0
- package/dist/connector/mcp/image-admission.js.map +1 -0
- package/dist/connector/mcp/stdio.d.ts +13 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +66 -23
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +3 -0
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/constants/tools/index.js +1 -1
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/execution/base.d.ts +13 -0
- package/dist/execution/base.d.ts.map +1 -1
- package/dist/execution/base.js +103 -8
- package/dist/execution/base.js.map +1 -1
- package/dist/execution/errors.d.ts +12 -0
- package/dist/execution/errors.d.ts.map +1 -0
- package/dist/execution/errors.js +21 -0
- package/dist/execution/errors.js.map +1 -0
- package/dist/execution/factory.d.ts.map +1 -1
- package/dist/execution/factory.js +1 -0
- package/dist/execution/factory.js.map +1 -1
- package/dist/execution/hybrid.d.ts +5 -0
- package/dist/execution/hybrid.d.ts.map +1 -1
- package/dist/execution/hybrid.js +36 -16
- package/dist/execution/hybrid.js.map +1 -1
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/local.d.ts +13 -0
- package/dist/execution/local.d.ts.map +1 -1
- package/dist/execution/local.js +145 -19
- package/dist/execution/local.js.map +1 -1
- package/dist/execution/output.d.ts +27 -0
- package/dist/execution/output.d.ts.map +1 -0
- package/dist/execution/output.js +105 -0
- package/dist/execution/output.js.map +1 -0
- package/dist/execution/remote.d.ts +26 -2
- package/dist/execution/remote.d.ts.map +1 -1
- package/dist/execution/remote.js +86 -15
- package/dist/execution/remote.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -3
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/process/kill-tree.d.ts +11 -13
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +11 -13
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +31 -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/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 +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +7 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +201 -54
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/run/command-gate.d.ts.map +1 -1
- package/dist/run/command-gate.js +11 -0
- package/dist/run/command-gate.js.map +1 -1
- package/dist/run/workspace-fingerprint.d.ts +4 -4
- package/dist/run/workspace-fingerprint.d.ts.map +1 -1
- package/dist/run/workspace-fingerprint.js +6 -4
- package/dist/run/workspace-fingerprint.js.map +1 -1
- package/dist/runtime/bidi/index.d.ts +1 -1
- package/dist/runtime/bidi/index.d.ts.map +1 -1
- package/dist/runtime/bidi/index.js +1 -1
- package/dist/runtime/bidi/index.js.map +1 -1
- package/dist/runtime/bidi/session.d.ts +19 -0
- package/dist/runtime/bidi/session.d.ts.map +1 -1
- package/dist/runtime/bidi/session.js +150 -26
- package/dist/runtime/bidi/session.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +43 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +626 -161
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +46 -4
- package/dist/runtime/query/index.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 +84 -22
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +33 -13
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts +5 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +66 -1
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +45 -16
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +6 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +4 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +193 -41
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/delegating.d.ts +2 -1
- package/dist/scheduler/delegating.d.ts.map +1 -1
- package/dist/scheduler/delegating.js +6 -5
- package/dist/scheduler/delegating.js.map +1 -1
- package/dist/scheduler/local.d.ts +2 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +2 -2
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +12 -4
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts +17 -3
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +153 -34
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +33 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +39 -2
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/run-code.d.ts.map +1 -1
- package/dist/tools/builtins/run-code.js +9 -11
- package/dist/tools/builtins/run-code.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +2 -1
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +250 -6
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +100 -30
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/types/agent/manager.d.ts +1 -1
- package/dist/types/agent/manager.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +10 -1
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/authorization/index.d.ts +12 -12
- package/dist/types/bidi/index.d.ts +19 -2
- package/dist/types/bidi/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.d.ts +18 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/connector/execution-context.d.ts +3 -1
- package/dist/types/connector/execution-context.d.ts.map +1 -1
- package/dist/types/execution/index.d.ts +43 -1
- package/dist/types/execution/index.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +11 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/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 +2 -1
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +14 -1
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +1 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/interface.d.ts +14 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -10
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +77 -6
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +5 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/abort.d.ts +8 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +16 -0
- package/dist/utils/abort.js.map +1 -1
- package/package.json +4 -4
- package/src/authorization/gate.ts +1 -1
- package/src/connector/index.ts +2 -0
- package/src/connector/mcp/adapter.ts +82 -21
- package/src/connector/mcp/client.ts +2 -1
- package/src/connector/mcp/http-redirect.ts +32 -0
- package/src/connector/mcp/http-sse.ts +5 -0
- package/src/connector/mcp/image-admission.ts +272 -0
- package/src/connector/mcp/stdio.ts +63 -21
- package/src/connector/mcp/streamable-http.ts +3 -0
- package/src/constants/tools/index.ts +1 -1
- package/src/execution/base.ts +124 -7
- package/src/execution/errors.ts +24 -0
- package/src/execution/factory.ts +1 -0
- package/src/execution/hybrid.ts +47 -16
- package/src/execution/index.ts +1 -0
- package/src/execution/local.ts +164 -19
- package/src/execution/output.ts +104 -0
- package/src/execution/remote.ts +122 -17
- package/src/plugin/lifecycle.ts +9 -3
- package/src/process/kill-tree.ts +11 -13
- package/src/provider/fallback.ts +39 -0
- package/src/provider/idle-timeout.ts +8 -0
- package/src/provider/retry.ts +8 -0
- package/src/public-runtime.ts +7 -1
- package/src/registry/tool/execute.ts +230 -64
- package/src/run/command-gate.ts +12 -0
- package/src/run/workspace-fingerprint.ts +5 -4
- package/src/runtime/bidi/index.ts +1 -1
- package/src/runtime/bidi/session.ts +177 -28
- package/src/runtime/query/executor.ts +769 -163
- package/src/runtime/query/index.ts +53 -6
- package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
- package/src/runtime/query/request-rich-content.ts +39 -14
- package/src/runtime/query/resume-pending.ts +83 -1
- package/src/runtime/query/tool-output-budget.ts +52 -17
- package/src/runtime/query/tooling.ts +10 -0
- package/src/sandbox/provider/local.ts +213 -45
- package/src/scheduler/delegating.ts +6 -5
- package/src/scheduler/local.ts +3 -2
- package/src/session/workspace/driver.ts +12 -4
- package/src/session/workspace/git-worktree.ts +173 -34
- package/src/skills/registry.ts +47 -0
- package/src/tools/builtins/computer-use.ts +45 -2
- package/src/tools/builtins/run-code.ts +9 -11
- package/src/tools/builtins/skill.ts +325 -7
- package/src/tools/coordinator/agent.ts +108 -33
- package/src/types/agent/manager.ts +1 -1
- package/src/types/agent/scheduler.ts +10 -1
- package/src/types/bidi/index.ts +19 -2
- package/src/types/computer-use/index.ts +19 -0
- package/src/types/connector/execution-context.ts +3 -0
- package/src/types/execution/index.ts +46 -1
- package/src/types/hitl/index.ts +11 -0
- package/src/types/message/index.ts +3 -2
- package/src/types/plugin/index.ts +15 -0
- package/src/types/provider/interface.ts +18 -0
- package/src/types/sandbox/index.ts +15 -10
- package/src/types/tool/index.ts +85 -6
- package/src/types/workspace/ref.ts +5 -0
- package/src/utils/abort.ts +18 -0
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* See session-hierarchy.md §7.2 (Git-worktree reference backend).
|
|
6
6
|
*
|
|
7
|
-
* Safety: every subprocess invocation uses `execFile` with an argv array
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Safety: every subprocess invocation uses `execFile` with an argv array and
|
|
8
|
+
* separates caller-controlled positional arguments from Git options with
|
|
9
|
+
* `--`. Filesystem paths are canonicalized and confined to the configured
|
|
10
|
+
* worktree root before Git receives them. Failures surface as
|
|
10
11
|
* {@link WorkspaceBackendError} carrying the underlying cause.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { execFile } from 'node:child_process'
|
|
14
|
-
import {
|
|
15
|
+
import { realpath } from 'node:fs/promises'
|
|
16
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
15
17
|
import { promisify } from 'node:util'
|
|
16
18
|
import { NAMZU } from '../../constants/telemetry/index.js'
|
|
17
19
|
import type { GitWorktreeBackendMeta, WorkspaceRef } from '../../types/workspace/ref.js'
|
|
@@ -71,8 +73,8 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
71
73
|
private readonly exec: ExecFile
|
|
72
74
|
|
|
73
75
|
constructor(config: GitWorktreeDriverConfig) {
|
|
74
|
-
this.repoRoot = config.repoRoot
|
|
75
|
-
this.worktreesDir = config.worktreesDir ?? join(config.repoRoot, '.namzu', 'worktrees')
|
|
76
|
+
this.repoRoot = resolve(config.repoRoot)
|
|
77
|
+
this.worktreesDir = resolve(config.worktreesDir ?? join(config.repoRoot, '.namzu', 'worktrees'))
|
|
76
78
|
this.log = config.logger.child({ [SCOPE_ATTRIBUTE]: 'session/workspace/git-worktree' })
|
|
77
79
|
this.exec = config.execFile ?? defaultExecFile
|
|
78
80
|
}
|
|
@@ -80,10 +82,38 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
80
82
|
async create(params: CreateWorkspaceParams): Promise<WorkspaceRef> {
|
|
81
83
|
const id = generateWorkspaceId()
|
|
82
84
|
const label = params.label ?? id
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
let repoRoot: string
|
|
86
|
+
let worktreesDir: string
|
|
87
|
+
let worktreePath: string
|
|
88
|
+
try {
|
|
89
|
+
repoRoot = await canonicalizeThroughExisting(this.repoRoot)
|
|
90
|
+
worktreesDir = await canonicalizeThroughExisting(this.worktreesDir)
|
|
91
|
+
const lexicalPath = resolve(worktreesDir, label)
|
|
92
|
+
if (!isStrictDescendant(worktreesDir, lexicalPath)) {
|
|
93
|
+
throw new Error(`Workspace label escapes the managed worktree directory: ${label}`)
|
|
94
|
+
}
|
|
95
|
+
worktreePath = await canonicalizeThroughExisting(lexicalPath)
|
|
96
|
+
if (!isStrictDescendant(worktreesDir, worktreePath)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Workspace label resolves through a link outside the managed worktree directory: ${label}`,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
if (relative(worktreesDir, lexicalPath) !== relative(worktreesDir, worktreePath)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Workspace label resolves through a link to a different managed path: ${label}`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
throw new WorkspaceBackendError({ op: 'create:ownership', kind: this.kind, cause })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const relativePath = relative(worktreesDir, worktreePath).split(sep).join('/')
|
|
111
|
+
const branch = `namzu/${relativePath}`
|
|
85
112
|
|
|
86
|
-
|
|
113
|
+
// `--` belongs before caller-controlled positional arguments. Without it,
|
|
114
|
+
// a base ref such as `--no-checkout` is accepted as a Git option and the
|
|
115
|
+
// driver publishes a checkout containing only its `.git` link.
|
|
116
|
+
const argv = ['-C', repoRoot, 'worktree', 'add', '-b', branch, '--', worktreePath]
|
|
87
117
|
if (params.baseRef !== undefined) {
|
|
88
118
|
argv.push(params.baseRef)
|
|
89
119
|
}
|
|
@@ -103,7 +133,7 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
103
133
|
// The bar is deliberately high: registered under this exact path
|
|
104
134
|
// AND carrying the branch this call asked for. A registered path
|
|
105
135
|
// alone can be a half-finished checkout, or somebody else's.
|
|
106
|
-
if (!(await this.createdDespite(worktreePath, branch))) {
|
|
136
|
+
if (!(await this.createdDespite(repoRoot, worktreePath, branch))) {
|
|
107
137
|
throw new WorkspaceBackendError({ op: 'create', kind: this.kind, cause })
|
|
108
138
|
}
|
|
109
139
|
this.log.warn('git-worktree add reported failure but the worktree is present', {
|
|
@@ -115,7 +145,7 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
115
145
|
|
|
116
146
|
const meta: GitWorktreeBackendMeta = {
|
|
117
147
|
backend: 'git-worktree',
|
|
118
|
-
repoRoot
|
|
148
|
+
repoRoot,
|
|
119
149
|
branch,
|
|
120
150
|
worktreePath,
|
|
121
151
|
}
|
|
@@ -135,34 +165,50 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
135
165
|
async branch(source: WorkspaceRef, params: BranchWorkspaceParams): Promise<WorkspaceRef> {
|
|
136
166
|
// Branch from the source worktree's current branch. The source's
|
|
137
167
|
// `meta.branch` is the ref we base off.
|
|
138
|
-
|
|
168
|
+
const owned = await this.requireOwnedRef(source, 'branch')
|
|
169
|
+
return this.create({ baseRef: owned.branch, label: params.label })
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
async dispose(ref: WorkspaceRef): Promise<void> {
|
|
173
|
+
const owned = await this.requireOwnedRef(ref, 'dispose')
|
|
174
|
+
const registered = await this.registeredEntry(owned, 'dispose:list')
|
|
175
|
+
if (!registered) {
|
|
176
|
+
this.log.debug('git-worktree already gone; dispose idempotent', {
|
|
177
|
+
[NAMZU.SESSION_ID]: ref.id,
|
|
178
|
+
'namzu.session.path': owned.worktreePath,
|
|
179
|
+
})
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
142
183
|
// Tolerate missing directories per roadmap Risk #3 mitigation: the
|
|
143
184
|
// broadcast-rollback compensating step calls dispose on partially
|
|
144
|
-
// provisioned refs and must not propagate "already gone" errors.
|
|
185
|
+
// provisioned refs and must not propagate "already gone" errors. The
|
|
186
|
+
// repository list, rather than an error-message regex, is the evidence:
|
|
187
|
+
// an unavailable repository can contain the same "No such file" words.
|
|
145
188
|
try {
|
|
146
189
|
await this.exec('git', [
|
|
147
190
|
'-C',
|
|
148
|
-
|
|
191
|
+
owned.repoRoot,
|
|
149
192
|
'worktree',
|
|
150
193
|
'remove',
|
|
151
|
-
|
|
194
|
+
owned.worktreePath,
|
|
152
195
|
'--force',
|
|
153
196
|
])
|
|
154
197
|
this.log.info('git-worktree disposed', {
|
|
155
198
|
[NAMZU.SESSION_ID]: ref.id,
|
|
156
|
-
'namzu.session.path':
|
|
199
|
+
'namzu.session.path': owned.worktreePath,
|
|
157
200
|
})
|
|
158
201
|
} catch (cause) {
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
202
|
+
// Two concurrent disposals can both observe the entry before one wins.
|
|
203
|
+
// Re-read after failure and accept only repository evidence that the
|
|
204
|
+
// exact owned branch/path pair is now absent.
|
|
205
|
+
const nowRegistered = await this.registeredEntry(owned, 'dispose:recheck').catch(
|
|
206
|
+
() => undefined,
|
|
207
|
+
)
|
|
208
|
+
if (nowRegistered === null) {
|
|
163
209
|
this.log.debug('git-worktree already gone; dispose idempotent', {
|
|
164
210
|
[NAMZU.SESSION_ID]: ref.id,
|
|
165
|
-
'namzu.session.path':
|
|
211
|
+
'namzu.session.path': owned.worktreePath,
|
|
166
212
|
})
|
|
167
213
|
return
|
|
168
214
|
}
|
|
@@ -184,15 +230,13 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
184
230
|
* has already gone wrong once, and guessing optimistically there is how
|
|
185
231
|
* a recovery turns a bad situation into a wrong one.
|
|
186
232
|
*/
|
|
187
|
-
private async createdDespite(
|
|
233
|
+
private async createdDespite(
|
|
234
|
+
repoRoot: string,
|
|
235
|
+
worktreePath: string,
|
|
236
|
+
branch: string,
|
|
237
|
+
): Promise<boolean> {
|
|
188
238
|
try {
|
|
189
|
-
const { stdout } = await this.exec('git', [
|
|
190
|
-
'-C',
|
|
191
|
-
this.repoRoot,
|
|
192
|
-
'worktree',
|
|
193
|
-
'list',
|
|
194
|
-
'--porcelain',
|
|
195
|
-
])
|
|
239
|
+
const { stdout } = await this.exec('git', ['-C', repoRoot, 'worktree', 'list', '--porcelain'])
|
|
196
240
|
const entry = parseWorktreeList(stdout, worktreePath)
|
|
197
241
|
// `--porcelain` writes the branch as a full ref (`refs/heads/x`),
|
|
198
242
|
// and `branch` here is the short name this call passed to `-b`.
|
|
@@ -206,11 +250,12 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
206
250
|
}
|
|
207
251
|
|
|
208
252
|
async inspect(ref: WorkspaceRef): Promise<WorkspaceInspection> {
|
|
253
|
+
const owned = await this.requireOwnedRef(ref, 'inspect')
|
|
209
254
|
let listStdout: string
|
|
210
255
|
try {
|
|
211
256
|
const result = await this.exec('git', [
|
|
212
257
|
'-C',
|
|
213
|
-
|
|
258
|
+
owned.repoRoot,
|
|
214
259
|
'worktree',
|
|
215
260
|
'list',
|
|
216
261
|
'--porcelain',
|
|
@@ -220,14 +265,14 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
220
265
|
throw new WorkspaceBackendError({ op: 'inspect:list', kind: this.kind, cause })
|
|
221
266
|
}
|
|
222
267
|
|
|
223
|
-
const entry = parseWorktreeList(listStdout,
|
|
224
|
-
if (!entry) {
|
|
225
|
-
return { exists: false, currentRef:
|
|
268
|
+
const entry = parseWorktreeList(listStdout, owned.worktreePath)
|
|
269
|
+
if (!entry || entry.branch !== `refs/heads/${owned.branch}`) {
|
|
270
|
+
return { exists: false, currentRef: owned.branch, isDirty: false }
|
|
226
271
|
}
|
|
227
272
|
|
|
228
273
|
let statusStdout: string
|
|
229
274
|
try {
|
|
230
|
-
const result = await this.exec('git', ['-C',
|
|
275
|
+
const result = await this.exec('git', ['-C', owned.worktreePath, 'status', '--porcelain'])
|
|
231
276
|
statusStdout = result.stdout
|
|
232
277
|
} catch (cause) {
|
|
233
278
|
throw new WorkspaceBackendError({ op: 'inspect:status', kind: this.kind, cause })
|
|
@@ -239,6 +284,100 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
239
284
|
isDirty: statusStdout.trim().length > 0,
|
|
240
285
|
}
|
|
241
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Turn persisted metadata back into an owned driver capability.
|
|
290
|
+
*
|
|
291
|
+
* A WorkspaceRef is recovery data, not authority to make this driver act on
|
|
292
|
+
* any worktree registered in the same repository. Existing links are
|
|
293
|
+
* resolved before the boundary is decided, and every Git call receives the
|
|
294
|
+
* canonical path that was checked. This closes static symlink escapes. It
|
|
295
|
+
* does not claim protection from a host process concurrently swapping path
|
|
296
|
+
* components or mount points; the Git CLI exposes no descriptor-relative
|
|
297
|
+
* removal primitive with which to make that guarantee.
|
|
298
|
+
*/
|
|
299
|
+
private async requireOwnedRef(ref: WorkspaceRef, op: string): Promise<GitWorktreeBackendMeta> {
|
|
300
|
+
try {
|
|
301
|
+
if (ref.meta.backend !== this.kind) {
|
|
302
|
+
throw new Error(`Workspace backend ${ref.meta.backend} does not belong to ${this.kind}`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const repoRoot = await canonicalizeThroughExisting(this.repoRoot)
|
|
306
|
+
const refRepoRoot = await canonicalizeThroughExisting(ref.meta.repoRoot)
|
|
307
|
+
if (refRepoRoot !== repoRoot) {
|
|
308
|
+
throw new Error('Workspace reference belongs to a different repository')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const worktreesDir = await canonicalizeThroughExisting(this.worktreesDir)
|
|
312
|
+
const worktreePath = await canonicalizeThroughExisting(ref.meta.worktreePath)
|
|
313
|
+
if (!isStrictDescendant(worktreesDir, worktreePath)) {
|
|
314
|
+
throw new Error('Workspace reference escapes the managed worktree directory')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const label = relative(worktreesDir, worktreePath).split(sep).join('/')
|
|
318
|
+
const branch = `namzu/${label}`
|
|
319
|
+
if (ref.meta.branch !== branch) {
|
|
320
|
+
throw new Error('Workspace reference branch does not match its managed path')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
backend: 'git-worktree',
|
|
325
|
+
repoRoot,
|
|
326
|
+
branch,
|
|
327
|
+
worktreePath,
|
|
328
|
+
}
|
|
329
|
+
} catch (cause) {
|
|
330
|
+
throw new WorkspaceBackendError({ op: `${op}:ownership`, kind: this.kind, cause })
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private async registeredEntry(
|
|
335
|
+
owned: GitWorktreeBackendMeta,
|
|
336
|
+
op: string,
|
|
337
|
+
): Promise<{ path: string; head?: string; branch?: string } | null> {
|
|
338
|
+
let stdout: string
|
|
339
|
+
try {
|
|
340
|
+
const result = await this.exec('git', [
|
|
341
|
+
'-C',
|
|
342
|
+
owned.repoRoot,
|
|
343
|
+
'worktree',
|
|
344
|
+
'list',
|
|
345
|
+
'--porcelain',
|
|
346
|
+
])
|
|
347
|
+
stdout = result.stdout
|
|
348
|
+
} catch (cause) {
|
|
349
|
+
throw new WorkspaceBackendError({ op, kind: this.kind, cause })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const entry = parseWorktreeList(stdout, owned.worktreePath)
|
|
353
|
+
return entry?.branch === `refs/heads/${owned.branch}` ? entry : null
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Resolve every existing path component, preserving only a missing suffix. */
|
|
358
|
+
async function canonicalizeThroughExisting(input: string): Promise<string> {
|
|
359
|
+
let existing = resolve(input)
|
|
360
|
+
const remainder: string[] = []
|
|
361
|
+
|
|
362
|
+
for (;;) {
|
|
363
|
+
try {
|
|
364
|
+
const canonical = await realpath(existing)
|
|
365
|
+
return resolve(canonical, ...remainder)
|
|
366
|
+
} catch (cause) {
|
|
367
|
+
const code = (cause as NodeJS.ErrnoException).code
|
|
368
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') throw cause
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const parent = dirname(existing)
|
|
372
|
+
if (parent === existing) return resolve(existing, ...remainder)
|
|
373
|
+
remainder.unshift(basename(existing))
|
|
374
|
+
existing = parent
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isStrictDescendant(root: string, candidate: string): boolean {
|
|
379
|
+
const rel = relative(root, candidate)
|
|
380
|
+
return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)
|
|
242
381
|
}
|
|
243
382
|
|
|
244
383
|
/**
|
package/src/skills/registry.ts
CHANGED
|
@@ -207,6 +207,53 @@ export class SkillRegistry {
|
|
|
207
207
|
return [...this.skills.values()]
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The model-facing catalog, keyed by what this registry actually accepts.
|
|
212
|
+
*
|
|
213
|
+
* Reading the Map key is load-bearing for host namespaces. A plugin may file
|
|
214
|
+
* `reconcile` as `ledger__reconcile`; reconstructing the name from the skill
|
|
215
|
+
* body would advertise a call that cannot resolve (and could collide with a
|
|
216
|
+
* different plugin's unqualified name).
|
|
217
|
+
*/
|
|
218
|
+
async catalog(): Promise<
|
|
219
|
+
readonly {
|
|
220
|
+
registeredName: string
|
|
221
|
+
description: string
|
|
222
|
+
location: string
|
|
223
|
+
allowedTools?: string
|
|
224
|
+
invocation?: 'model' | 'operator' | 'both'
|
|
225
|
+
}[]
|
|
226
|
+
> {
|
|
227
|
+
const entries: Array<{
|
|
228
|
+
registeredName: string
|
|
229
|
+
description: string
|
|
230
|
+
location: string
|
|
231
|
+
allowedTools?: string
|
|
232
|
+
invocation?: 'model' | 'operator' | 'both'
|
|
233
|
+
}> = []
|
|
234
|
+
// `load` owns file freshness and deletion. Listing straight from the Map
|
|
235
|
+
// would make an edited SKILL.md stale here while a named read in the same
|
|
236
|
+
// run saw the new metadata — two catalogs depending on which operation the
|
|
237
|
+
// model happened to try first.
|
|
238
|
+
for (const registeredName of [...this.skills.keys()]) {
|
|
239
|
+
const loaded = await this.load(registeredName, 'metadata')
|
|
240
|
+
if (!loaded) continue
|
|
241
|
+
const skill = loaded.skill
|
|
242
|
+
entries.push({
|
|
243
|
+
registeredName,
|
|
244
|
+
description: skill.metadata.description,
|
|
245
|
+
location: join(skill.dirPath, SKILL_FILENAME),
|
|
246
|
+
...(skill.metadata.allowedTools === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { allowedTools: skill.metadata.allowedTools }),
|
|
249
|
+
...(skill.metadata.invocation === undefined
|
|
250
|
+
? {}
|
|
251
|
+
: { invocation: skill.metadata.invocation }),
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
return entries
|
|
255
|
+
}
|
|
256
|
+
|
|
210
257
|
/**
|
|
211
258
|
* Every registered name.
|
|
212
259
|
*
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ComputerUseAction,
|
|
4
4
|
ComputerUseCapabilities,
|
|
5
5
|
ComputerUseHost,
|
|
6
|
+
ComputerUseOutcomeUnknown,
|
|
6
7
|
ComputerUseResult,
|
|
7
8
|
} from '../../types/computer-use/index.js'
|
|
8
9
|
import type { ToolDefinition, ToolResult } from '../../types/tool/index.js'
|
|
@@ -132,6 +133,41 @@ function resultToToolResult(result: ComputerUseResult): ToolResult {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
function isOutcomeUnknown(
|
|
137
|
+
value: unknown,
|
|
138
|
+
action: ComputerUseAction['type'],
|
|
139
|
+
): value is ComputerUseOutcomeUnknown {
|
|
140
|
+
if (typeof value !== 'object' || value === null) return false
|
|
141
|
+
const candidate = value as Partial<ComputerUseOutcomeUnknown>
|
|
142
|
+
return (
|
|
143
|
+
candidate.code === 'computer_use_outcome_unknown' &&
|
|
144
|
+
candidate.action === action &&
|
|
145
|
+
candidate.outcome === 'unknown' &&
|
|
146
|
+
candidate.retrySafety === 'unsafe' &&
|
|
147
|
+
typeof candidate.timedOut === 'boolean' &&
|
|
148
|
+
typeof candidate.exitCode === 'number' &&
|
|
149
|
+
Number.isInteger(candidate.exitCode) &&
|
|
150
|
+
typeof candidate.message === 'string' &&
|
|
151
|
+
candidate.message.length > 0
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function unknownOutcomeToToolResult(error: ComputerUseOutcomeUnknown): ToolResult {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
output: '',
|
|
159
|
+
error: error.message,
|
|
160
|
+
data: {
|
|
161
|
+
code: error.code,
|
|
162
|
+
action: error.action,
|
|
163
|
+
outcome: error.outcome,
|
|
164
|
+
retrySafety: error.retrySafety,
|
|
165
|
+
timedOut: error.timedOut,
|
|
166
|
+
exitCode: error.exitCode,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
135
171
|
/**
|
|
136
172
|
* Factory: given a ComputerUseHost (provided by the consumer — e.g.
|
|
137
173
|
* @namzu/computer-use's SubprocessComputerUseHost), returns a ToolDefinition
|
|
@@ -172,8 +208,15 @@ export function createComputerUseTool(host: ComputerUseHost): ToolDefinition<Act
|
|
|
172
208
|
error: `computer_use: action "${input.type}" requires capability "${required}" which is not available on this host (displayServer=${host.capabilities.displayServer}).`,
|
|
173
209
|
}
|
|
174
210
|
}
|
|
175
|
-
|
|
176
|
-
|
|
211
|
+
try {
|
|
212
|
+
const result = await host.execute(input as ComputerUseAction)
|
|
213
|
+
return resultToToolResult(result)
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (isOutcomeUnknown(error, input.type)) {
|
|
216
|
+
return unknownOutcomeToToolResult(error)
|
|
217
|
+
}
|
|
218
|
+
throw error
|
|
219
|
+
}
|
|
177
220
|
},
|
|
178
221
|
})
|
|
179
222
|
}
|
|
@@ -13,12 +13,12 @@ import { defineTool } from '../defineTool.js'
|
|
|
13
13
|
* loop cannot reach further than the twenty calls could have.
|
|
14
14
|
*
|
|
15
15
|
* **The program's reach is the RUN's reach, and nothing wider.** Every
|
|
16
|
-
* capability it can call is a tool already in this run's registry
|
|
17
|
-
* narrowed by the turn's `allowedTools
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* capability it can call is a tool already in this run's registry and
|
|
17
|
+
* narrowed by the turn's `allowedTools`. The host dispatch records every
|
|
18
|
+
* child in the run and applies the run's operator authorization gate. An
|
|
19
|
+
* explicit allow proceeds; a denial or an undecided child fails closed and
|
|
20
|
+
* leaves a durable refusal, because an already-executing parent cannot open
|
|
21
|
+
* a second durable human-review turn on the program's behalf.
|
|
22
22
|
*
|
|
23
23
|
* Opt-in, and not in the default builtin set. A run that does not need
|
|
24
24
|
* model-authored control flow should not have a way to execute
|
|
@@ -100,11 +100,9 @@ export function buildRunCodeTool(options: RunCodeToolOptions = {}) {
|
|
|
100
100
|
maxOutputBytes: options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT,
|
|
101
101
|
...(context.abortSignal ? { signal: context.abortSignal } : {}),
|
|
102
102
|
onHostCall: async (request, operation) => {
|
|
103
|
-
// Through the
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// forgot it would be the one a model reached through a
|
|
107
|
-
// program.
|
|
103
|
+
// Through the run-owned nested dispatch: registry narrowing,
|
|
104
|
+
// operator authorization, audit/event lineage and invocation
|
|
105
|
+
// cancellation remain host-owned rather than worker-owned.
|
|
108
106
|
try {
|
|
109
107
|
const toolResult = await dispatch(request.name, request.input, {
|
|
110
108
|
signal: operation.signal,
|