@namzu/sdk 26.0.0 → 27.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 (93) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/dist/compaction/manager.d.ts +22 -0
  3. package/dist/compaction/manager.d.ts.map +1 -1
  4. package/dist/compaction/manager.js +31 -3
  5. package/dist/compaction/manager.js.map +1 -1
  6. package/dist/connector/mcp/adapter.d.ts +7 -1
  7. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  8. package/dist/connector/mcp/adapter.js +12 -1
  9. package/dist/connector/mcp/adapter.js.map +1 -1
  10. package/dist/connector/mcp/index.d.ts +1 -0
  11. package/dist/connector/mcp/index.d.ts.map +1 -1
  12. package/dist/connector/mcp/index.js +3 -0
  13. package/dist/connector/mcp/index.js.map +1 -1
  14. package/dist/connector/mcp/prompt-adapter.d.ts +10 -4
  15. package/dist/connector/mcp/prompt-adapter.d.ts.map +1 -1
  16. package/dist/connector/mcp/prompt-adapter.js +10 -4
  17. package/dist/connector/mcp/prompt-adapter.js.map +1 -1
  18. package/dist/connector/mcp/server-stdio.d.ts +62 -0
  19. package/dist/connector/mcp/server-stdio.d.ts.map +1 -0
  20. package/dist/connector/mcp/server-stdio.js +121 -0
  21. package/dist/connector/mcp/server-stdio.js.map +1 -0
  22. package/dist/eval/experiment.d.ts +0 -6
  23. package/dist/eval/experiment.d.ts.map +1 -1
  24. package/dist/eval/experiment.js +28 -0
  25. package/dist/eval/experiment.js.map +1 -1
  26. package/dist/eval/index.d.ts +2 -0
  27. package/dist/eval/index.d.ts.map +1 -1
  28. package/dist/eval/index.js +1 -0
  29. package/dist/eval/index.js.map +1 -1
  30. package/dist/eval/types.d.ts +17 -0
  31. package/dist/eval/types.d.ts.map +1 -1
  32. package/dist/eval/uncertainty.d.ts +64 -0
  33. package/dist/eval/uncertainty.d.ts.map +1 -0
  34. package/dist/eval/uncertainty.js +92 -0
  35. package/dist/eval/uncertainty.js.map +1 -0
  36. package/dist/public-runtime.d.ts +1 -0
  37. package/dist/public-runtime.d.ts.map +1 -1
  38. package/dist/public-runtime.js +1 -0
  39. package/dist/public-runtime.js.map +1 -1
  40. package/dist/registry/tool/execute.d.ts.map +1 -1
  41. package/dist/registry/tool/execute.js +2 -1
  42. package/dist/registry/tool/execute.js.map +1 -1
  43. package/dist/session/workspace/git-worktree.d.ts +15 -0
  44. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  45. package/dist/session/workspace/git-worktree.js +55 -1
  46. package/dist/session/workspace/git-worktree.js.map +1 -1
  47. package/dist/tools/builtins/bash.d.ts.map +1 -1
  48. package/dist/tools/builtins/bash.js +35 -0
  49. package/dist/tools/builtins/bash.js.map +1 -1
  50. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  51. package/dist/tools/coordinator/agent.js +12 -0
  52. package/dist/tools/coordinator/agent.js.map +1 -1
  53. package/dist/tools/coordinator/index.d.ts.map +1 -1
  54. package/dist/tools/coordinator/index.js +134 -6
  55. package/dist/tools/coordinator/index.js.map +1 -1
  56. package/dist/tools/trusted-read-only.d.ts +42 -0
  57. package/dist/tools/trusted-read-only.d.ts.map +1 -0
  58. package/dist/tools/trusted-read-only.js +51 -0
  59. package/dist/tools/trusted-read-only.js.map +1 -0
  60. package/dist/tools/untrusted-envelope.d.ts +22 -2
  61. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  62. package/dist/tools/untrusted-envelope.js +22 -2
  63. package/dist/tools/untrusted-envelope.js.map +1 -1
  64. package/dist/types/sandbox/index.d.ts +23 -0
  65. package/dist/types/sandbox/index.d.ts.map +1 -1
  66. package/dist/types/sandbox/index.js.map +1 -1
  67. package/dist/types/tool/index.d.ts +28 -0
  68. package/dist/types/tool/index.d.ts.map +1 -1
  69. package/dist/types/tool/index.js.map +1 -1
  70. package/dist/verification/rules.d.ts.map +1 -1
  71. package/dist/verification/rules.js +5 -1
  72. package/dist/verification/rules.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/compaction/manager.ts +39 -3
  75. package/src/connector/mcp/adapter.ts +11 -0
  76. package/src/connector/mcp/index.ts +3 -0
  77. package/src/connector/mcp/prompt-adapter.ts +10 -4
  78. package/src/connector/mcp/server-stdio.ts +137 -0
  79. package/src/eval/experiment.ts +30 -0
  80. package/src/eval/index.ts +2 -0
  81. package/src/eval/types.ts +17 -0
  82. package/src/eval/uncertainty.ts +124 -0
  83. package/src/public-runtime.ts +1 -0
  84. package/src/registry/tool/execute.ts +2 -1
  85. package/src/session/workspace/git-worktree.ts +55 -1
  86. package/src/tools/builtins/bash.ts +34 -0
  87. package/src/tools/coordinator/agent.ts +12 -0
  88. package/src/tools/coordinator/index.ts +144 -8
  89. package/src/tools/trusted-read-only.ts +52 -0
  90. package/src/tools/untrusted-envelope.ts +22 -2
  91. package/src/types/sandbox/index.ts +23 -0
  92. package/src/types/tool/index.ts +30 -0
  93. package/src/verification/rules.ts +5 -1
@@ -157,6 +157,73 @@ const askUserQuestionModelInputSchema: Record<string, unknown> = {
157
157
  additionalProperties: false,
158
158
  }
159
159
 
160
+ /** One well-formed tag token: `<step>`, `</step>`, `<a href="…">`, `<br/>`. */
161
+ const TAG_TOKEN = /<\/?[A-Za-z][\w-]*(?:\s[^<>]*)?\/?>/g
162
+ const DESCRIPTION_BLOCK = /<description>([\s\S]*?)<\/description>/gi
163
+
164
+ /**
165
+ * Remove every tag token, including the ones removing a tag creates.
166
+ *
167
+ * One pass is not enough and the reason is not obvious: deleting an inner
168
+ * tag can splice its neighbours into a new one. `<<step>step>` loses the
169
+ * inner `<step>` and the halves close up into `<step>` again, so a line
170
+ * that is nothing but markup comes back non-empty and is offered to a
171
+ * human as a step to approve — which is the exact outcome this whole path
172
+ * exists to prevent.
173
+ *
174
+ * Repeating to a fixed point terminates: every pass that changes the
175
+ * string removes at least one token and so strictly shortens it.
176
+ *
177
+ * Only ever used to ANSWER "is there anything here besides markup". The
178
+ * result is never shown to anyone, so this is a test rather than a
179
+ * sanitiser, and it does not have to defend against every way a tag can
180
+ * be spelled.
181
+ */
182
+ function withoutTags(text: string): string {
183
+ let current = text
184
+ for (;;) {
185
+ const next = current.replace(TAG_TOKEN, '')
186
+ if (next === current) return current
187
+ current = next
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Peel tag wrappers off the ENDS of one line, and nowhere else — a step
193
+ * that legitimately says "wrap it in a <div>" keeps its sentence.
194
+ */
195
+ function unwrapStepLine(line: string): string {
196
+ let text = line.trim()
197
+ for (;;) {
198
+ const next = text
199
+ .replace(/^<[A-Za-z][\w-]*(?:\s[^<>]*)?>\s*/, '')
200
+ .replace(/\s*<\/[A-Za-z][\w-]*>$/, '')
201
+ .trim()
202
+ if (next === text) break
203
+ text = next
204
+ }
205
+ return text
206
+ }
207
+
208
+ /**
209
+ * A step list the model serialized instead of building.
210
+ *
211
+ * The line-splitting fallback below is the general case, and it had one
212
+ * shape badly wrong. A model that serializes this array tends to reach for
213
+ * MARKUP, not for prose:
214
+ *
215
+ * <steps>
216
+ * <step>
217
+ * <description>Convert the document to Word</description>
218
+ * </step>
219
+ * </steps>
220
+ *
221
+ * Split on newlines, that is seven "steps", five of which are tags. A host
222
+ * then numbered them in an approval card and asked a person to approve
223
+ * `</steps>` — reported from a real run. The descriptions the model named
224
+ * are right there, so read them; fall back to lines only when there are
225
+ * none, and drop the lines that carry no words at all.
226
+ */
160
227
  function normalizeApprovePlanSteps(value: unknown): unknown {
161
228
  if (typeof value !== 'string') return value
162
229
 
@@ -171,19 +238,84 @@ function normalizeApprovePlanSteps(value: unknown): unknown {
171
238
  }
172
239
  }
173
240
 
241
+ const described = [...trimmed.matchAll(DESCRIPTION_BLOCK)]
242
+ .map((match) => (match[1] ?? '').trim())
243
+ .filter(Boolean)
244
+ if (described.length > 0) {
245
+ return described.map((description) => ({ description }))
246
+ }
247
+
174
248
  const lines = trimmed
175
249
  .split(/\r?\n+/)
176
250
  .map((line) =>
177
- line
178
- .trim()
179
- .replace(/^(?:[-*•]|\d+[.)])\s*/, '')
180
- .trim(),
251
+ unwrapStepLine(
252
+ line
253
+ .trim()
254
+ .replace(/^(?:[-*•]|\d+[.)])\s*/, '')
255
+ .trim(),
256
+ ),
181
257
  )
182
- .filter(Boolean)
258
+ .filter((line) => line.length > 0 && withoutTags(line).trim().length > 0)
259
+
260
+ // Every line was markup: there is no plan in this string, and inventing
261
+ // one step reading `<steps>` is worse than saying so.
262
+ if (lines.length === 0) {
263
+ return withoutTags(unwrapStepLine(trimmed)).trim()
264
+ ? [{ description: unwrapStepLine(trimmed) }]
265
+ : []
266
+ }
183
267
 
184
- return (lines.length ? lines : [trimmed]).map((description) => ({
185
- description,
186
- }))
268
+ return lines.map((description) => ({ description }))
269
+ }
270
+
271
+ /**
272
+ * The single closed shape a capable provider constrains this call to —
273
+ * the same instrument `ask_user_question` carries, for the same failure.
274
+ *
275
+ * `steps` arriving as a STRING is what everything above exists to survive,
276
+ * and surviving it is not the same as preventing it: the normalizer can
277
+ * only guess at a structure the model already threw away. Advertising the
278
+ * closed shape turns the guess into a refusal at generation time.
279
+ */
280
+ const approvePlanModelInputSchema: Record<string, unknown> = {
281
+ type: 'object',
282
+ properties: {
283
+ title: {
284
+ type: 'string',
285
+ description: 'Short title for the plan (e.g. "TypeScript Security & Performance Review").',
286
+ },
287
+ summary: {
288
+ type: 'string',
289
+ description: '1-3 sentence summary of what you plan to do.',
290
+ },
291
+ steps: {
292
+ type: 'array',
293
+ description:
294
+ 'A JSON array of ordered step objects. Never a string, and never markup — no <step> or <description> tags.',
295
+ items: {
296
+ type: 'object',
297
+ properties: {
298
+ description: {
299
+ type: 'string',
300
+ description: 'What this step does, as one plain sentence a person can read.',
301
+ },
302
+ agent_id: {
303
+ type: 'string',
304
+ description: 'Which agent handles this; omit for steps you carry out yourself.',
305
+ },
306
+ depends_on: {
307
+ type: 'array',
308
+ items: { type: 'string' },
309
+ description: 'Descriptions of the steps that must finish before this one.',
310
+ },
311
+ },
312
+ required: ['description'],
313
+ additionalProperties: false,
314
+ },
315
+ },
316
+ },
317
+ required: ['title', 'summary', 'steps'],
318
+ additionalProperties: false,
187
319
  }
188
320
 
189
321
  /**
@@ -1013,6 +1145,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
1013
1145
  .preprocess(normalizeApprovePlanSteps, z.array(approvePlanStepSchema))
1014
1146
  .describe('Ordered list of planned steps'),
1015
1147
  }),
1148
+ modelInputSchema: structuredClone(approvePlanModelInputSchema),
1149
+ enforceModelInput: true,
1150
+ validationErrorHint:
1151
+ 'Required shape: {"title":"…","summary":"…","steps":[{"description":"One plain sentence"}]}. "steps" must be a JSON array of objects — never a string, and never markup such as <step> or <description>.',
1016
1152
  category: 'custom',
1017
1153
  permissions: [],
1018
1154
  readOnly: true,
@@ -0,0 +1,52 @@
1
+ import type { ToolDefinition } from '../types/tool/index.js'
2
+
3
+ /**
4
+ * May this tool's read-only claim settle a gate on its own?
5
+ *
6
+ * A connected server declares whether its own tools are read-only, and
7
+ * that declaration decided whether a call was approved without asking. The
8
+ * thing being gated supplied the input to the gate.
9
+ *
10
+ * The wire itself calls these fields HINTS. Three separate consumers read
11
+ * them as facts, so a server setting `readOnlyHint: true` and
12
+ * `destructiveHint: false` controlled the whole predicate for its own
13
+ * tools — the kernel's `allow_read_only` rule, the operator prompt
14
+ * exemption, and the plan-mode pass.
15
+ *
16
+ * The estate floor this repository inherits already decided this: least
17
+ * privilege, default deny, and — on tool results and fetched content —
18
+ * data is not instructions, and untrusted content cannot escalate
19
+ * capabilities. A server's declaration about its own tools is untrusted
20
+ * content by that definition. This is that rule applied, not a new policy.
21
+ *
22
+ * **The asymmetry is the design.** A self-declaration may RAISE the
23
+ * requirement and never LOWER it:
24
+ *
25
+ * - `destructiveHint: true` from a server is believed. A server
26
+ * volunteering that its tool is dangerous moves toward caution, and
27
+ * disbelieving it buys nothing.
28
+ * - `readOnlyHint: true` from a server does not, on its own, settle a
29
+ * call as allowed or skip a prompt. That is the untrusted party
30
+ * opening its own gate.
31
+ *
32
+ * Trust for the second case comes from the operator, per server, and is
33
+ * recorded on the tool as `provenance.readOnlyHintTrusted`. Never a global
34
+ * switch: one flag meaning "trust annotations" hands every connected
35
+ * server the same reach, which is the hole restated.
36
+ *
37
+ * `isReadOnly` itself is left reporting faithfully what the server said.
38
+ * Provenance and policy are different questions, and collapsing them would
39
+ * corrupt the outbound re-export and the prompt's own destructive label in
40
+ * order to fix a gate.
41
+ */
42
+ export function isTrustedReadOnly(tool: ToolDefinition | undefined, input: unknown): boolean {
43
+ if (!tool?.isReadOnly) return false
44
+
45
+ // No provenance means the tool is host-defined: it came from this
46
+ // process, from code the operator installed, and there is no untrusted
47
+ // party in the chain. Requiring an opt-in for a builtin would break
48
+ // every read-only exemption for no gain in trust.
49
+ if (tool.provenance && !tool.provenance.readOnlyHintTrusted) return false
50
+
51
+ return tool.isReadOnly(input)
52
+ }
@@ -2,12 +2,32 @@
2
2
  * Framing for content the agent did not author and must not obey.
3
3
  *
4
4
  * An unlabelled block of text in a tool result reads exactly like the agent's
5
- * own instructions. The mitigation that survives contact with a real model is
6
- * not filtering — it is saying plainly whose words these are and that they are
5
+ * own instructions. This says plainly whose words these are and that they are
7
6
  * material rather than direction. That is the floor this estate already
8
7
  * states: data is not instructions, and a tool result cannot escalate what an
9
8
  * agent may do.
10
9
  *
10
+ * **It marks provenance. It refuses nothing, and it does not stop an
11
+ * attacker who is trying.** This paragraph used to claim the framing was
12
+ * "the mitigation that survives contact with a real model", and that is
13
+ * measurably wrong. Nasr et al., "The Attacker Moves Second"
14
+ * (arXiv:2510.09023), broke twelve published defences at above 90% attack
15
+ * success once the attacker adapts; the majority had originally reported
16
+ * near-zero success. Delimiting specifically goes from as low as 1% under
17
+ * a static benchmark to above 95% under adaptive attack.
18
+ *
19
+ * So read every number for a prompt-level defence as static unless it says
20
+ * otherwise, and treat this envelope as raising cost rather than as a
21
+ * boundary. What survives an adapting attacker in the same literature is
22
+ * architectural: AgentDojo (arXiv:2406.13352) found tool isolation and
23
+ * tool filtering the effective mitigations, and this repository's real
24
+ * boundaries are of that kind — the permission gate, the sandbox, the
25
+ * egress proxy deciding by resolved address.
26
+ *
27
+ * The two details below still matter. They are what stops the framing
28
+ * being trivially removable by the content itself, which is a lower bar
29
+ * than stopping an attacker and worth clearing anyway.
30
+ *
11
31
  * Two details make the difference between a boundary and a decoration, and
12
32
  * both were missing from this repo's first envelope:
13
33
  *
@@ -103,6 +103,29 @@ export interface SandboxExecOptions {
103
103
  readonly timeout?: number
104
104
  readonly env?: Record<string, string>
105
105
  readonly cwd?: string
106
+ /**
107
+ * Called as output arrives, before the command has finished.
108
+ *
109
+ * Every container-tier worker already streams its output a chunk at a
110
+ * time — the wire carries `stdout_delta` and `stderr_delta` events —
111
+ * and every backend concatenated them into a string and returned that
112
+ * when the process exited. So a command that takes eight minutes said
113
+ * nothing for eight minutes, on a transport that had been reporting
114
+ * the whole time.
115
+ *
116
+ * Additive and optional: a backend that cannot stream simply never
117
+ * calls it, and `SandboxExecResult.stdout` still carries the complete
118
+ * output either way. A caller that wants only the result ignores this
119
+ * and behaves exactly as before.
120
+ *
121
+ * The callback must not throw and must not be awaited — it is on the
122
+ * read path of a running process, so a slow or failing consumer would
123
+ * otherwise become a slow or failing command.
124
+ */
125
+ readonly onOutput?: (chunk: {
126
+ readonly stream: 'stdout' | 'stderr'
127
+ readonly data: string
128
+ }) => void
106
129
  /**
107
130
  * Cancellation for the command. A backend that honours it kills the
108
131
  * process; one that does not simply ignores it, so this is additive.
@@ -322,6 +322,36 @@ export interface ToolDefinition<TInput = unknown> {
322
322
  isReadOnly?(input: TInput): boolean
323
323
  isDestructive?(input: TInput): boolean
324
324
  isConcurrencySafe?(input: TInput): boolean
325
+
326
+ /**
327
+ * Where this tool came from, when it did not come from here.
328
+ *
329
+ * Absent means host-defined: this process, code the operator installed,
330
+ * no untrusted party in the chain. Present means a connected server
331
+ * supplied both the tool and its own description of what the tool does
332
+ * — including whether it is read-only, which three separate gates were
333
+ * treating as a fact rather than as the hint the wire calls it.
334
+ *
335
+ * See {@link isTrustedReadOnly}. This field exists so a gate can tell
336
+ * the two apart; `isReadOnly` keeps reporting faithfully what the
337
+ * server said, because the outbound re-export and the destructive
338
+ * label shown to a human both need the server's own answer.
339
+ */
340
+ provenance?: ToolProvenance
341
+ }
342
+
343
+ export interface ToolProvenance {
344
+ /** The connected server this tool came from, named as configured. */
345
+ readonly server: string
346
+ /**
347
+ * The operator marked this server's read-only claims as trustworthy.
348
+ *
349
+ * Per server, never global: one switch meaning "trust annotations"
350
+ * hands every connected server the same reach, which is the hole it
351
+ * would be closing. Default false — an unmarked server's claim raises
352
+ * the requirement and never lowers it.
353
+ */
354
+ readonly readOnlyHintTrusted: boolean
325
355
  }
326
356
 
327
357
  export type ToolPermission =
@@ -1,4 +1,5 @@
1
1
  import { DANGEROUS_PATTERNS } from '../constants/tools/index.js'
2
+ import { isTrustedReadOnly } from '../tools/trusted-read-only.js'
2
3
  import type { ToolDefinition } from '../types/tool/index.js'
3
4
  import type { GateDecision, VerificationRule } from '../types/verification/index.js'
4
5
 
@@ -12,7 +13,10 @@ export function evaluateRule(
12
13
  ): GateDecision | null {
13
14
  switch (rule.type) {
14
15
  case 'allow_read_only': {
15
- return toolDef?.isReadOnly?.(toolInput) ? 'allow' : null
16
+ // A server's own claim about its own tool cannot settle this. See
17
+ // `isTrustedReadOnly`: a self-declaration may raise the requirement
18
+ // and never lower it.
19
+ return isTrustedReadOnly(toolDef, toolInput) ? 'allow' : null
16
20
  }
17
21
 
18
22
  case 'deny_dangerous_patterns': {