@sema-agent/core 7.17.7 → 7.19.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 (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -1,11 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Type } from "typebox";
3
- import { ARTIFACT_FAILURE_RESULT_CODE, ARTIFACT_HOST_ERROR_CODE, ARTIFACT_LIST_SCOPES, ARTIFACT_PLANE_CONTENT_ORIGIN, ARTIFACT_PUBLISH_CONTEXTS, } from "../../core/artifact-host.js";
3
+ import { ARTIFACT_ACTION_CONCURRENCY_SAFE, ARTIFACT_COMMIT_THROW_OUTCOME, ARTIFACT_FAILURE_RESULT_CODE, ARTIFACT_HOST_ERROR_CODE, ARTIFACT_LIST_SCOPES, ARTIFACT_PLANE_CONTENT_ORIGIN, ARTIFACT_PUBLISH_CONTEXTS, } from "../../core/artifact-host.js";
4
4
  import { ARTIFACT_TOOL_NAME, toolFace } from "../../core/tool-catalog-entries.js";
5
+ import { resolveAsk } from "../../core/tool-policy.js";
5
6
  import { errorResult } from "../../core/tools.js";
6
7
  import { inlineUntrusted } from "../../core/untrusted-text.js";
7
8
  import { actionDescription, artifactDescription, PARAM_TEXT, urlDescription } from "./artifact-text.js";
8
- const MAX_FILES_PER_PUBLISH = 255;
9
+ import { ARTIFACT_REJECT_BREAKER_THRESHOLD, artifactRejectBannerFor, artifactRejectKeyOf, createArtifactRejectBreaker } from "./reject-breaker.js";
10
+ export const MAX_FILES_PER_PUBLISH = 255;
9
11
  const LIST_LIMIT_DEFAULT = 25;
10
12
  const LIST_LIMIT_MAX = 100;
11
13
  const BINARY_NOTE_MEDIA = new Set(["application/json", "application/javascript", "text/javascript", "image/svg+xml", "application/xml"]);
@@ -125,36 +127,42 @@ function buildSchema(caps) {
125
127
  const invalid = (text) => errorResult(`Error (Artifact): ${text}`, { type: "artifact", invalidArguments: true });
126
128
  const typed = (code, text, extra = {}) => errorResult(`Error (Artifact): ${text}`, { type: "artifact", code: ARTIFACT_FAILURE_RESULT_CODE[code], ...extra });
127
129
  export function createArtifactTool(opts) {
128
- const { host, capabilities: caps, runId, taskId, sessionId, agentName, readLocalFile } = opts;
130
+ const { host, capabilities: caps, runId, taskId, sessionId, agentName, readLocalFile, onAsk } = opts;
129
131
  const versions = new Map();
130
132
  const bindings = new Map();
131
133
  const occurrences = new Map();
134
+ const breaker = createArtifactRejectBreaker();
135
+ const ownUrls = new Set();
136
+ const sharedUrls = new Set();
137
+ const sharedReadApproved = new Set();
138
+ const consentInFlight = new Map();
139
+ const activeTargets = new Map();
132
140
  const hasAction = (a) => caps.actions.includes(a);
133
141
  const rememberRef = (ref) => {
134
142
  versions.set(ref.url, ref.version);
135
143
  };
136
- const attemptIdOf = (ctx) => {
144
+ const rememberOwner = (url, owner) => {
145
+ (owner === "mine" ? ownUrls : sharedUrls).add(url);
146
+ };
147
+ const nextContext = (ctx) => {
137
148
  const n = occurrences.get(ctx.toolCallId) ?? 0;
138
149
  occurrences.set(ctx.toolCallId, n + 1);
139
- return artifactAttemptId(runId, ctx.toolCallId, n);
150
+ return {
151
+ taskId,
152
+ ...(sessionId !== undefined ? { sessionId } : {}),
153
+ ...(agentName !== undefined ? { agentName } : {}),
154
+ runId,
155
+ attemptId: artifactAttemptId(runId, ctx.toolCallId, n),
156
+ };
140
157
  };
141
- const contextFor = (attemptId) => ({
142
- taskId,
143
- ...(sessionId !== undefined ? { sessionId } : {}),
144
- ...(agentName !== undefined ? { agentName } : {}),
145
- runId,
146
- attemptId,
147
- });
148
158
  const hostError = (verb, fault, url) => {
149
- if (caps.plane === "local-stub" && fault.kind === "threw") {
150
- return errorResult(`Error (Artifact): the local artifact stub failed before committing (${inlineUntrusted(fault.message, 200)}). Nothing was published; the call can be repeated.`, {
151
- type: "artifact", code: ARTIFACT_HOST_ERROR_CODE, outcome: "not_executed", plane: caps.plane,
152
- });
159
+ const outcome = fault.kind === "threw" ? ARTIFACT_COMMIT_THROW_OUTCOME[caps.commit] : "unknown";
160
+ const details = { type: "artifact", code: ARTIFACT_HOST_ERROR_CODE, outcome, plane: caps.plane, commit: caps.commit };
161
+ if (outcome === "not_executed") {
162
+ return errorResult(`Error (Artifact): the artifact host failed before committing (${inlineUntrusted(fault.message, 200)}). Nothing was published; the call can be repeated.`, details);
153
163
  }
154
164
  const verifiable = hasAction("read") && url !== "";
155
- return errorResult(`Error (Artifact): the artifact host did not answer the ${verb.slice(0, 20)} (${inlineUntrusted(fault.message, 200)}). Whether it was applied is not known.${verifiable ? ` Verify with action: "read" on ${url.slice(0, 200)} before repeating the call.` : ""}`, {
156
- type: "artifact", code: ARTIFACT_HOST_ERROR_CODE, outcome: "unknown", plane: caps.plane,
157
- });
165
+ return errorResult(`Error (Artifact): the artifact host did not answer the ${verb.slice(0, 20)} (${inlineUntrusted(fault.message, 200)}). Whether it was applied is not known: it may already have been committed, so do not simply repeat the call.${verifiable ? ` Verify with action: "read" on ${url.slice(0, 200)} first.` : ""}`, details);
158
166
  };
159
167
  const notFoundOrForbidden = (code, url, message) => code === "not_found"
160
168
  ? typed(code, `no artifact at ${inlineUntrusted(url, 200)} (${inlineUntrusted(message, 200)}).`, { url })
@@ -173,42 +181,60 @@ export function createArtifactTool(opts) {
173
181
  }
174
182
  }
175
183
  };
184
+ const manifestOf = (a) => {
185
+ const mainNorm = normalizePublishedPath(basenameOf(a.file_path));
186
+ if (!mainNorm.ok)
187
+ return { ok: false, result: invalid(`the published name of ${inlineUntrusted(a.file_path, 200)} ${mainNorm.reason.slice(0, 80)}.`) };
188
+ const entries = [];
189
+ if (a.files === undefined || (Array.isArray(a.files) && a.files.length === 0))
190
+ return { ok: true, main: mainNorm.path, entries };
191
+ if (!caps.multiFile)
192
+ return { ok: false, result: invalid("this artifact host publishes single files only; `files` is not accepted.") };
193
+ if (!Array.isArray(a.files))
194
+ return { ok: false, result: invalid("`files` must be an array of { path, file_path } entries.") };
195
+ if (a.files.length > MAX_FILES_PER_PUBLISH)
196
+ return { ok: false, result: invalid(`\`files\` carries ${String(a.files.length).slice(0, 20)} entries; at most ${MAX_FILES_PER_PUBLISH} supporting files are accepted per publish.`) };
197
+ const seen = new Set([mainNorm.path.toLowerCase()]);
198
+ for (const entry of a.files) {
199
+ if (!isRecord(entry) || typeof entry["path"] !== "string" || typeof entry["file_path"] !== "string")
200
+ return { ok: false, result: invalid("every `files` entry must carry a string `path` and a string `file_path`.") };
201
+ const norm = normalizePublishedPath(entry["path"]);
202
+ if (!norm.ok)
203
+ return { ok: false, result: invalid(`files entry ${inlineUntrusted(entry["path"], 120)} ${norm.reason.slice(0, 80)}.`) };
204
+ const fold = norm.path.toLowerCase();
205
+ if (seen.has(fold))
206
+ return { ok: false, result: invalid(`files entry ${inlineUntrusted(norm.path, 120)} collides with another published path.`) };
207
+ seen.add(fold);
208
+ entries.push({ path: norm.path, file_path: entry["file_path"] });
209
+ }
210
+ return { ok: true, main: mainNorm.path, entries };
211
+ };
176
212
  async function publish(a, ctx) {
177
- const attemptId = attemptIdOf(ctx);
178
- if (a.file_path === undefined)
213
+ if (typeof a.file_path !== "string")
179
214
  return invalid("`file_path` is required to publish.");
215
+ const manifest = manifestOf(a);
216
+ if (!manifest.ok)
217
+ return manifest.result;
180
218
  const files = [];
181
219
  const budget = Math.min(caps.maxFileBytes, caps.maxTotalBytes);
182
220
  const first = await readLocalFile(a.file_path, budget, ctx.signal);
183
221
  if (!first.ok)
184
222
  return readRefusal(a.file_path, first);
185
- files.push({ path: basenameOf(a.file_path), content: first.bytes, mediaType: mediaTypeOf(a.file_path) });
223
+ files.push({ path: manifest.main, content: first.bytes, mediaType: mediaTypeOf(manifest.main) });
186
224
  let total = first.bytes.byteLength;
187
225
  const url = a.url ?? bindings.get(first.key);
188
226
  if (url === undefined && (a.force === true || a.baseVersion !== undefined)) {
189
227
  return invalid("`force` and `baseVersion` only apply to an update — pass `url`, or republish the same file path this session already published.");
190
228
  }
191
- if (a.files !== undefined && a.files.length > 0) {
192
- if (!caps.multiFile)
193
- return invalid("this artifact host publishes single files only; `files` is not accepted.");
194
- const seen = new Set([files[0].path.normalize("NFC").toLowerCase()]);
195
- for (const entry of a.files) {
196
- const norm = normalizePublishedPath(entry.path);
197
- if (!norm.ok)
198
- return invalid(`files entry ${inlineUntrusted(entry.path, 120)} ${norm.reason.slice(0, 80)}.`);
199
- const fold = norm.path.toLowerCase();
200
- if (seen.has(fold))
201
- return invalid(`files entry ${inlineUntrusted(norm.path, 120)} collides with another published path.`);
202
- seen.add(fold);
203
- const left = caps.maxTotalBytes - total;
204
- if (left <= 0)
205
- return typed("payload_too_large", `the files already total ${String(total).slice(0, 20)} bytes; this host accepts at most ${String(caps.maxTotalBytes).slice(0, 20)} bytes per version.`, { limit: caps.maxTotalBytes, measured: total });
206
- const r = await readLocalFile(entry.file_path, Math.min(caps.maxFileBytes, left), ctx.signal);
207
- if (!r.ok)
208
- return readRefusal(entry.file_path, r);
209
- total += r.bytes.byteLength;
210
- files.push({ path: norm.path, content: r.bytes, mediaType: mediaTypeOf(norm.path) });
211
- }
229
+ for (const entry of manifest.entries) {
230
+ const left = caps.maxTotalBytes - total;
231
+ if (left <= 0)
232
+ return typed("payload_too_large", `the files already total ${String(total).slice(0, 20)} bytes; this host accepts at most ${String(caps.maxTotalBytes).slice(0, 20)} bytes per version.`, { limit: caps.maxTotalBytes, measured: total });
233
+ const r = await readLocalFile(entry.file_path, Math.min(caps.maxFileBytes, left), ctx.signal);
234
+ if (!r.ok)
235
+ return readRefusal(entry.file_path, r);
236
+ total += r.bytes.byteLength;
237
+ files.push({ path: entry.path, content: r.bytes, mediaType: mediaTypeOf(entry.path) });
212
238
  }
213
239
  const baseVersion = a.baseVersion ?? (url !== undefined ? versions.get(url) : undefined);
214
240
  const req = {
@@ -222,12 +248,13 @@ export function createArtifactTool(opts) {
222
248
  ...(a.force === true ? { force: true } : {}),
223
249
  publishContext: a.publishContext ?? "plain",
224
250
  };
225
- const call = await callHost("publish", () => host.publish(req, contextFor(attemptId)), ctx.signal);
251
+ const call = await callHost("publish", () => host.publish(req, nextContext(ctx)), ctx.signal);
226
252
  if (!call.ok)
227
253
  return hostError("publish", call, inlineUntrusted(url ?? "", 200));
228
254
  const out = call.value;
229
255
  if (out.ok) {
230
256
  rememberRef(out.ref);
257
+ rememberOwner(out.ref.url, "mine");
231
258
  bindings.set(first.key, out.ref.url);
232
259
  const title = out.ref.title !== undefined ? ` — ${inlineUntrusted(out.ref.title, 120)}` : "";
233
260
  return {
@@ -236,7 +263,12 @@ export function createArtifactTool(opts) {
236
263
  };
237
264
  }
238
265
  if (out.code === "conflict") {
239
- const snapshot = out.current;
266
+ let snapshot = out.current;
267
+ if (snapshot !== "reread") {
268
+ const withheld = url === undefined || (await filesOwnership(url, ctx)) !== undefined;
269
+ if (withheld || (url !== undefined && sharedUrls.has(url) && !sharedReadApproved.has(url)))
270
+ snapshot = "reread";
271
+ }
240
272
  if (snapshot !== "reread" && url !== undefined)
241
273
  versions.set(url, out.currentVersion);
242
274
  const handedBack = snapshot === "reread"
@@ -266,56 +298,125 @@ export function createArtifactTool(opts) {
266
298
  }
267
299
  }
268
300
  async function list(a, ctx) {
269
- const attemptId = attemptIdOf(ctx);
270
301
  const scope = a.scope ?? "mine";
271
302
  const limit = a.limit ?? LIST_LIMIT_DEFAULT;
272
- const call = await callHost("list", () => host.list({ scope, limit }, contextFor(attemptId)), ctx.signal);
303
+ const call = await callHost("list", () => host.list({ scope, limit }, nextContext(ctx)), ctx.signal);
273
304
  if (!call.ok)
274
305
  return hostError("list", call, "");
275
306
  const out = call.value;
276
307
  if (!out.ok)
277
308
  return typed("refused", `the host refused the listing (${inlineUntrusted(out.message, 200)}).`);
309
+ for (const r of out.rows)
310
+ rememberOwner(r.url, r.owner);
278
311
  const lines = out.rows.map((r) => `${inlineUntrusted(r.url, 300)} v${inlineUntrusted(r.version, 40)} ${inlineUntrusted(r.updatedAt, 40)} (${r.owner})${r.title !== undefined ? ` ${inlineUntrusted(r.title, 120)}` : ""}`);
279
312
  const body = lines.length === 0 ? "nothing listed" : lines.join("\n");
280
313
  return { content: `${body}${out.truncated ? "\n(more artifacts exist; raise `limit` to see them)" : ""}`, details: { type: "artifact", action: "list", plane: caps.plane, scope, rows: out.rows, truncated: out.truncated } };
281
314
  }
315
+ const sharedReadConsent = (url, ref, ctx) => {
316
+ if (sharedReadApproved.has(url))
317
+ return Promise.resolve({ ok: true });
318
+ const pending = consentInFlight.get(url);
319
+ if (pending !== undefined)
320
+ return pending;
321
+ const ask = (async () => {
322
+ const title = ref?.title !== undefined ? ` (${inlineUntrusted(ref.title, 120)})` : "";
323
+ const version = ref !== undefined ? `, version ${inlineUntrusted(ref.version, 60)}` : "";
324
+ const resolved = await resolveAsk({
325
+ toolName: ARTIFACT_TOOL_NAME,
326
+ toolCallId: ctx.toolCallId,
327
+ args: { action: "read", url },
328
+ message: `Read a shared artifact: ${inlineUntrusted(url, 300)}${title}${version} — an artifact another person shared with this user; approving covers re-reads of it for the rest of this run.`,
329
+ requiresRealApproval: true,
330
+ }, onAsk, ctx.signal);
331
+ if (resolved.action !== "allow")
332
+ return { ok: false, reason: resolved.message ?? "not approved" };
333
+ if (resolved.updatedInput !== undefined)
334
+ return { ok: false, reason: "the approval edited the request; an edited approval does not authorize the original target" };
335
+ sharedReadApproved.add(url);
336
+ return { ok: true };
337
+ })().finally(() => {
338
+ if (!activeTargets.has(url))
339
+ consentInFlight.delete(url);
340
+ });
341
+ consentInFlight.set(url, ask);
342
+ return ask;
343
+ };
344
+ const filesOwnership = async (url, ctx) => {
345
+ if (!ownUrls.has(url) && !sharedUrls.has(url)) {
346
+ if (!hasAction("read"))
347
+ return typed("refused", `${inlineUntrusted(url, 200)} is not an artifact this run published or listed as its own, and this host declares no read to establish who owns it; its files were not delivered.`, { url, sharedRead: "ownership_unknown" });
348
+ const probe = await callHost("read", () => host.read({ url }, nextContext(ctx)), ctx.signal);
349
+ if (!probe.ok)
350
+ return hostError("read", probe, inlineUntrusted(url, 200));
351
+ if (!probe.value.ok)
352
+ return notFoundOrForbidden(probe.value.code, url, probe.value.message);
353
+ if (probe.value.ref.url !== url)
354
+ return hostError("read", { kind: "malformed", message: "read.ref.url does not match the requested target" }, inlineUntrusted(url, 200));
355
+ rememberOwner(url, probe.value.isolatedSummary ? "shared" : "mine");
356
+ }
357
+ if (!sharedUrls.has(url))
358
+ return undefined;
359
+ const consent = await sharedReadConsent(url, undefined, ctx);
360
+ return consent.ok ? undefined : typed("refused", `reading the shared artifact ${inlineUntrusted(url, 200)} was not approved (${inlineUntrusted(consent.reason, 200)}); its files were not delivered.`, { url, sharedRead: "not_approved" });
361
+ };
282
362
  async function read(a, ctx) {
283
- const attemptId = attemptIdOf(ctx);
284
363
  if (a.url === undefined)
285
364
  return invalid("`url` is required to read an artifact.");
286
- const call = await callHost("read", () => host.read({ url: a.url }, contextFor(attemptId)), ctx.signal);
365
+ const call = await callHost("read", () => host.read({ url: a.url }, nextContext(ctx)), ctx.signal);
287
366
  if (!call.ok)
288
367
  return hostError("read", call, inlineUntrusted(a.url, 200));
289
368
  const out = call.value;
290
369
  if (!out.ok)
291
370
  return notFoundOrForbidden(out.code, a.url, out.message);
371
+ if (out.ref.url !== a.url)
372
+ return hostError("read", { kind: "malformed", message: "read.ref.url does not match the requested target" }, inlineUntrusted(a.url, 200));
373
+ rememberOwner(a.url, out.isolatedSummary ? "shared" : "mine");
374
+ if (sharedUrls.has(a.url)) {
375
+ const consent = await sharedReadConsent(a.url, out.ref, ctx);
376
+ if (!consent.ok)
377
+ return typed("refused", `reading the shared artifact ${inlineUntrusted(a.url, 200)} was not approved (${inlineUntrusted(consent.reason, 200)}); its content was not delivered.`, { url: a.url, sharedRead: "not_approved" });
378
+ }
292
379
  rememberRef(out.ref);
293
380
  const head = out.isolatedSummary ? `[isolated summary of a shared artifact — ${inlineUntrusted(out.ref.url, 300)}, version ${inlineUntrusted(out.ref.version, 60)}]\n` : "";
294
381
  return { content: `${head}${out.content}`, details: { type: "artifact", action: "read", plane: caps.plane, ref: out.ref, isolatedSummary: out.isolatedSummary } };
295
382
  }
296
383
  async function listFiles(a, ctx) {
297
- const attemptId = attemptIdOf(ctx);
298
384
  if (a.url === undefined)
299
385
  return invalid("`url` is required to list an artifact's files.");
300
- const call = await callHost("list_files", () => host.listFiles({ url: a.url }, contextFor(attemptId)), ctx.signal);
386
+ const gated = await filesOwnership(a.url, ctx);
387
+ if (gated !== undefined)
388
+ return gated;
389
+ const call = await callHost("list_files", () => host.listFiles({ url: a.url }, nextContext(ctx)), ctx.signal);
301
390
  if (!call.ok)
302
391
  return hostError("list_files", call, inlineUntrusted(a.url, 200));
303
392
  const out = call.value;
304
393
  if (!out.ok)
305
394
  return notFoundOrForbidden(out.code, a.url, out.message);
395
+ if (sharedUrls.has(a.url)) {
396
+ const denied = await filesOwnership(a.url, ctx);
397
+ if (denied !== undefined)
398
+ return denied;
399
+ }
306
400
  const lines = out.entries.map((e) => `${inlineUntrusted(e.path, 200)} ${String(e.bytes).slice(0, 20)} bytes${e.mediaType !== undefined ? ` ${inlineUntrusted(e.mediaType, 80)}` : ""}`);
307
401
  return { content: lines.length === 0 ? "no files" : lines.join("\n"), details: { type: "artifact", action: "list_files", plane: caps.plane, url: a.url, entries: out.entries } };
308
402
  }
309
403
  async function readFile(a, ctx) {
310
- const attemptId = attemptIdOf(ctx);
311
404
  if (a.url === undefined || a.path === undefined)
312
405
  return invalid("`url` and `path` are required to read a published file.");
313
- const call = await callHost("read_file", () => host.readFile({ url: a.url, path: a.path }, contextFor(attemptId)), ctx.signal);
406
+ const gated = await filesOwnership(a.url, ctx);
407
+ if (gated !== undefined)
408
+ return gated;
409
+ const call = await callHost("read_file", () => host.readFile({ url: a.url, path: a.path }, nextContext(ctx)), ctx.signal);
314
410
  if (!call.ok)
315
411
  return hostError("read_file", call, inlineUntrusted(a.url, 200));
316
412
  const out = call.value;
317
413
  if (!out.ok)
318
414
  return notFoundOrForbidden(out.code, a.url, out.message);
415
+ if (sharedUrls.has(a.url)) {
416
+ const denied = await filesOwnership(a.url, ctx);
417
+ if (denied !== undefined)
418
+ return denied;
419
+ }
319
420
  return { content: renderFile(out.file), details: { type: "artifact", action: "read_file", plane: caps.plane, url: a.url, path: out.file.path, bytes: out.file.content.byteLength, mediaType: out.file.mediaType } };
320
421
  }
321
422
  return {
@@ -325,27 +426,59 @@ export function createArtifactTool(opts) {
325
426
  description: artifactDescription(caps.plane, caps.actions),
326
427
  parameters: buildSchema(caps),
327
428
  prepareArguments: (args) => (isRecord(args) && args["action"] === undefined ? { ...args, action: "publish" } : args),
328
- execute: (args, ctx) => {
429
+ isConcurrencySafe: (args) => isRecord(args) && typeof args["action"] === "string" && ARTIFACT_ACTION_CONCURRENCY_SAFE[args["action"]] === true,
430
+ execute: async (args, ctx) => {
329
431
  const a = args;
330
432
  const action = a.action ?? "publish";
331
- if (!hasAction(action))
332
- return invalid(`action ${inlineUntrusted(String(action), 40)} is not available on this artifact host.`);
333
- switch (action) {
334
- case "publish":
335
- return publish(a, ctx);
336
- case "list":
337
- return list(a, ctx);
338
- case "read":
339
- return read(a, ctx);
340
- case "list_files":
341
- return listFiles(a, ctx);
342
- case "read_file":
343
- return readFile(a, ctx);
344
- default: {
345
- const _exhaustive = action;
346
- return _exhaustive;
433
+ const target = typeof a.url === "string" ? a.url : undefined;
434
+ if (target !== undefined)
435
+ activeTargets.set(target, (activeTargets.get(target) ?? 0) + 1);
436
+ let result;
437
+ try {
438
+ result = await dispatch(action, a, ctx);
439
+ }
440
+ finally {
441
+ if (target !== undefined) {
442
+ const left = activeTargets.get(target) - 1;
443
+ if (left > 0)
444
+ activeTargets.set(target, left);
445
+ else {
446
+ activeTargets.delete(target);
447
+ consentInFlight.delete(target);
448
+ }
347
449
  }
348
450
  }
451
+ if (typeof result === "string" || result.isError !== true) {
452
+ breaker.clear();
453
+ return result;
454
+ }
455
+ const details = isRecord(result.details) ? result.details : {};
456
+ const reason = typeof details["code"] === "string" ? details["code"] : details["invalidArguments"] === true ? "invalid_arguments" : "error";
457
+ const text = typeof result.content === "string" ? result.content : "";
458
+ const count = breaker.hit(artifactRejectKeyOf(a), reason, text);
459
+ if (count < ARTIFACT_REJECT_BREAKER_THRESHOLD || typeof result.content !== "string")
460
+ return result;
461
+ return { ...result, content: `${artifactRejectBannerFor(reason)}\n\n${result.content}`, details: { ...details, rejectStreak: count } };
349
462
  },
350
463
  };
464
+ function dispatch(action, a, ctx) {
465
+ if (!hasAction(action))
466
+ return invalid(`action ${inlineUntrusted(String(action), 40)} is not available on this artifact host.`);
467
+ switch (action) {
468
+ case "publish":
469
+ return publish(a, ctx);
470
+ case "list":
471
+ return list(a, ctx);
472
+ case "read":
473
+ return read(a, ctx);
474
+ case "list_files":
475
+ return listFiles(a, ctx);
476
+ case "read_file":
477
+ return readFile(a, ctx);
478
+ default: {
479
+ const _exhaustive = action;
480
+ return _exhaustive;
481
+ }
482
+ }
483
+ }
351
484
  }
@@ -0,0 +1,56 @@
1
+ import { type ArtifactAction, type ArtifactHost, type ArtifactHostCapabilities, type ArtifactRef } from "../../core/artifact-host.js";
2
+ export interface ArtifactConformanceHooks {
3
+ /** Make the host's NEXT write fail BEFORE its commit point (`arm`), then restore it (`disarm`). The atomicity vector
4
+ * runs only with this hook: the kit cannot reach inside an arbitrary host. */
5
+ precommitFault?: {
6
+ arm: () => void | Promise<void>;
7
+ disarm: () => void | Promise<void>;
8
+ };
9
+ /** Save a new version of `ref`'s page from INSIDE the page — the version a plain `force` must not discard
10
+ * (`force_refused`). The force-refusal vector runs only with this hook. */
11
+ pageSavedVersion?: (ref: ArtifactRef) => void | Promise<void>;
12
+ }
13
+ export interface ArtifactConformanceOptions {
14
+ hooks?: ArtifactConformanceHooks;
15
+ /** Run the at-limit POSITIVE arms of the byte-limit vector: they allocate and publish payloads of the declared
16
+ * sizes. Default `false` — reported `skipped`, named. The over-limit refusal arms always run. */
17
+ largePayloads?: boolean;
18
+ /** The run id the kit mounts (`onMount`) and calls under; default a fresh uuid. */
19
+ runId?: string;
20
+ }
21
+ export type ArtifactConformanceStatus = "pass" | "fail" | "not_applicable" | "skipped";
22
+ export interface ArtifactConformanceVector {
23
+ /** Stable id: a host's own conformance test keys on it. */
24
+ id: string;
25
+ describe: string;
26
+ /** Every listed action must be declared for the vector to apply. */
27
+ requiresActions: readonly ArtifactAction[];
28
+ requiresMultiFile?: true;
29
+ requiresHook?: keyof ArtifactConformanceHooks;
30
+ requiresLargePayloads?: true;
31
+ }
32
+ export interface ArtifactConformanceResult {
33
+ id: string;
34
+ status: ArtifactConformanceStatus;
35
+ /** What passed, what failed (the first violated expectation), or why the vector did not run. */
36
+ detail: string;
37
+ }
38
+ export interface ArtifactConformanceReport {
39
+ capabilities: ArtifactHostCapabilities;
40
+ runId: string;
41
+ results: readonly ArtifactConformanceResult[];
42
+ passed: number;
43
+ failed: number;
44
+ notApplicable: number;
45
+ skipped: number;
46
+ }
47
+ /** The full vector matrix (data; ids are stable). */
48
+ export declare const ARTIFACT_CONFORMANCE_VECTORS: readonly ArtifactConformanceVector[];
49
+ /** The vectors a host's DECLARATION covers (hook- and payload-gated vectors included: their gating is reported at run time). */
50
+ export declare function artifactConformanceVectors(host: ArtifactHost): ArtifactConformanceVector[];
51
+ /**
52
+ * Drive `host` through every vector its declaration covers, sequentially, under one fresh run id (the host's
53
+ * `onMount` is called first). Throws only when the declaration itself is invalid — every vector outcome, a thrown
54
+ * host method included, is a row of the report.
55
+ */
56
+ export declare function runArtifactConformance(host: ArtifactHost, opts?: ArtifactConformanceOptions): Promise<ArtifactConformanceReport>;