@sema-agent/core 5.12.0 → 5.14.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 (101) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +4 -0
  4. package/dist/agents/subagent.js +137 -43
  5. package/dist/brain/anthropic.js +33 -10
  6. package/dist/brain/context-overflow.d.ts +20 -0
  7. package/dist/brain/context-overflow.js +58 -0
  8. package/dist/brain/open-responses.js +24 -10
  9. package/dist/brain/openai.js +29 -11
  10. package/dist/brain/request-params.d.ts +2 -0
  11. package/dist/brain/request-params.js +16 -0
  12. package/dist/brain/stream-engine.d.ts +9 -1
  13. package/dist/brain/stream-engine.js +256 -27
  14. package/dist/brain/timeout.d.ts +1 -0
  15. package/dist/brain/timeout.js +1 -0
  16. package/dist/core/a2a.d.ts +2 -2
  17. package/dist/core/a2a.js +3 -3
  18. package/dist/core/ask-question.d.ts +47 -2
  19. package/dist/core/ask-question.js +209 -28
  20. package/dist/core/background-agent-store.d.ts +2 -0
  21. package/dist/core/checkpoint-store.d.ts +49 -18
  22. package/dist/core/checkpoint-store.js +117 -4
  23. package/dist/core/compliance.d.ts +11 -0
  24. package/dist/core/compliance.js +34 -0
  25. package/dist/core/governance-codes.d.ts +12 -0
  26. package/dist/core/governance-codes.js +24 -0
  27. package/dist/core/hooks.d.ts +24 -2
  28. package/dist/core/hooks.js +97 -10
  29. package/dist/core/human-input-projection.d.ts +12 -0
  30. package/dist/core/human-input-projection.js +27 -0
  31. package/dist/core/locked-config.d.ts +27 -0
  32. package/dist/core/locked-config.js +42 -0
  33. package/dist/core/mcp.d.ts +7 -2
  34. package/dist/core/mcp.js +7 -7
  35. package/dist/core/memory-admission.d.ts +51 -0
  36. package/dist/core/memory-admission.js +159 -0
  37. package/dist/core/memory.d.ts +2 -0
  38. package/dist/core/memory.js +3 -2
  39. package/dist/core/retention.d.ts +36 -0
  40. package/dist/core/retention.js +31 -0
  41. package/dist/core/runner/assemble-result.d.ts +1 -0
  42. package/dist/core/runner/assemble-result.js +2 -2
  43. package/dist/core/runner/prepare-memory.d.ts +9 -0
  44. package/dist/core/runner/prepare-memory.js +28 -2
  45. package/dist/core/runner/prepare-task.d.ts +18 -6
  46. package/dist/core/runner/prepare-task.js +428 -38
  47. package/dist/core/runner/runtask.d.ts +3 -6
  48. package/dist/core/runner/runtask.js +258 -77
  49. package/dist/core/runner/tool-output-projection.js +1 -0
  50. package/dist/core/session-store.d.ts +4 -0
  51. package/dist/core/session-store.js +5 -0
  52. package/dist/core/session.d.ts +1 -0
  53. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  54. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  55. package/dist/core/task-notification.d.ts +2 -0
  56. package/dist/core/task-notification.js +5 -3
  57. package/dist/core/task-registry-agent.d.ts +1 -0
  58. package/dist/core/task-registry-agent.js +6 -0
  59. package/dist/core/task-registry.d.ts +1 -0
  60. package/dist/core/task-registry.js +4 -1
  61. package/dist/core/tool-policy.d.ts +5 -0
  62. package/dist/core/tool-policy.js +2 -1
  63. package/dist/core/tool-result-store.d.ts +2 -0
  64. package/dist/core/tool-result-store.js +1 -0
  65. package/dist/core/types.d.ts +38 -2
  66. package/dist/core/wiring-manifest.d.ts +97 -0
  67. package/dist/core/wiring-manifest.js +186 -0
  68. package/dist/engine/compaction/compaction.js +2 -2
  69. package/dist/engine/harness/agent-harness.d.ts +2 -1
  70. package/dist/engine/harness/agent-harness.js +19 -2
  71. package/dist/engine/harness/types.d.ts +3 -1
  72. package/dist/engine/llm/types.d.ts +7 -0
  73. package/dist/engine/llm/types.js +8 -1
  74. package/dist/engine/llm/validation.js +11 -1
  75. package/dist/engine/session/import-validate.d.ts +6 -1
  76. package/dist/engine/session/import-validate.js +29 -6
  77. package/dist/engine/session/memory-repo.d.ts +3 -1
  78. package/dist/engine/session/memory-repo.js +2 -2
  79. package/dist/index.d.ts +12 -4
  80. package/dist/index.js +12 -4
  81. package/dist/internal/harness-types.d.ts +1 -1
  82. package/dist/internal/llm.d.ts +2 -2
  83. package/dist/internal/llm.js +1 -1
  84. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  85. package/dist/orchestration/run-workflow-tool.js +3 -0
  86. package/dist/orchestration/workflow-types.d.ts +8 -0
  87. package/dist/orchestration/workflow-types.js +14 -0
  88. package/dist/orchestration/workflow.d.ts +4 -0
  89. package/dist/orchestration/workflow.js +134 -5
  90. package/dist/prompts/default.js +1 -1
  91. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  92. package/dist/stores/file/checkpoint-store.js +31 -2
  93. package/dist/stores/file/index.js +1 -1
  94. package/dist/stores/file/session-store.d.ts +3 -1
  95. package/dist/stores/file/session-store.js +2 -2
  96. package/dist/stores/file/shared-ledger.js +8 -1
  97. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  98. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  99. package/dist/tools/fs/fs-bash.js +31 -12
  100. package/dist/tools/fs/safety.js +34 -10
  101. package/package.json +1 -1
@@ -0,0 +1,12 @@
1
+ export type GovernanceRetryClass = "transient" | "terminal";
2
+ export declare const GOVERNANCE_CODES: {
3
+ readonly "usage.window_exhausted": "terminal";
4
+ readonly "memory.admission_required": "transient";
5
+ readonly "memory.admission_denied": "terminal";
6
+ readonly "config.compliance_required": "transient";
7
+ readonly "config.compliance_denied": "terminal";
8
+ readonly "config.locked_key": "terminal";
9
+ };
10
+ export type GovernanceCode = keyof typeof GOVERNANCE_CODES;
11
+ export declare const NON_GOVERNANCE_MEMORY_CODES: ReadonlySet<string>;
12
+ export declare function governanceRetryClass(code: string): GovernanceRetryClass | undefined;
@@ -0,0 +1,24 @@
1
+ export const GOVERNANCE_CODES = {
2
+ "usage.window_exhausted": "terminal",
3
+ "memory.admission_required": "transient",
4
+ "memory.admission_denied": "terminal",
5
+ "config.compliance_required": "transient",
6
+ "config.compliance_denied": "terminal",
7
+ "config.locked_key": "terminal",
8
+ };
9
+ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
10
+ "memory.secret_detected",
11
+ "memory.promotable_ungated",
12
+ "memory.announce_failed",
13
+ "memory.partition_adopt_failed",
14
+ "memory.partition_split",
15
+ "memory.tail",
16
+ ]);
17
+ export function governanceRetryClass(code) {
18
+ if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
19
+ return GOVERNANCE_CODES[code];
20
+ }
21
+ if (code.startsWith("usage."))
22
+ return "terminal";
23
+ return undefined;
24
+ }
@@ -1,5 +1,5 @@
1
1
  import type { DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
2
- import type { SessionTreeEntry } from "../internal/harness-types.js";
2
+ import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
3
3
  import type { PermissionResult, ResolvedAsk, ToolCallRequest } from "./tool-policy.js";
4
4
  export interface Hooks {
5
5
  preToolUse?(toolName: string, input: unknown, ctx: HookToolContext): PreToolUseResult | undefined | Promise<PreToolUseResult | undefined>;
@@ -72,9 +72,17 @@ export interface StopHookResult {
72
72
  block?: string;
73
73
  additionalContext?: string;
74
74
  }
75
+ export interface HookEnvCapabilities {
76
+ canonicalPath?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
77
+ exists?(path: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
78
+ readLink?(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
79
+ cwd?(): string;
80
+ }
81
+ export declare function createHookEnvCapabilities(env: ExecutionEnv): HookEnvCapabilities;
75
82
  export interface HookToolContext {
76
83
  toolCallId: string;
77
84
  toolName: string;
85
+ env?: HookEnvCapabilities;
78
86
  }
79
87
  export interface HookToolOutput {
80
88
  content: Array<TextContent | ImageContent | DocumentContent>;
@@ -103,6 +111,18 @@ export interface ToolGateResult {
103
111
  };
104
112
  preToolContext: string[];
105
113
  }
114
+ export type ContentAskOutcome = {
115
+ kind: "answered";
116
+ presentedInput: unknown;
117
+ } | {
118
+ kind: "unavailable";
119
+ presentedInput?: unknown;
120
+ parkDeclined: boolean;
121
+ } | {
122
+ kind: "delivery_failure";
123
+ code: string;
124
+ presentedInput?: unknown;
125
+ };
106
126
  export interface ToolGateInput {
107
127
  onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
108
128
  event: {
@@ -111,9 +131,11 @@ export interface ToolGateInput {
111
131
  input: Record<string, unknown>;
112
132
  };
113
133
  preToolUse?: Hooks["preToolUse"];
134
+ hookEnv?: HookEnvCapabilities;
114
135
  adjudicate?: (req: ToolCallRequest) => Promise<PermissionResult>;
115
136
  resolveAsk: (decision: PermissionResult, req: ToolCallRequest) => Promise<ResolvedAsk>;
116
- suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown, safety?: import("./checkpoint-store.js").SafetyAxis, approverUnavailable?: boolean) => Promise<ToolGateResult["suspend"] | undefined>;
137
+ suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown, safety?: import("./checkpoint-store.js").SafetyAxis, liveFaceUnavailable?: boolean) => Promise<ToolGateResult["suspend"] | undefined>;
138
+ resolveContentAsk?: (req: ToolCallRequest) => Promise<ContentAskOutcome>;
117
139
  egress?: boolean;
118
140
  irreversibility?: "never" | "maybe" | "always";
119
141
  reversibilityProbe?: (args: unknown) => {
@@ -14,6 +14,67 @@ export function cloneObserverInput(input) {
14
14
  return input;
15
15
  }
16
16
  }
17
+ const reflectApply = Reflect.apply;
18
+ const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
19
+ const getPrototypeOf = Object.getPrototypeOf;
20
+ const objectPrototype = Object.prototype;
21
+ function findEnvSlot(env, key) {
22
+ for (let o = env; o !== null && o !== objectPrototype; o = getPrototypeOf(o)) {
23
+ const desc = getOwnPropertyDescriptor(o, key);
24
+ if (desc !== undefined)
25
+ return { holder: o, desc };
26
+ }
27
+ return undefined;
28
+ }
29
+ function readEnvPrimitive(env, key) {
30
+ const slot = findEnvSlot(env, key);
31
+ if (slot === undefined)
32
+ return undefined;
33
+ return slot.desc.get !== undefined ? reflectApply(slot.desc.get, env, []) : slot.desc.value;
34
+ }
35
+ function createCwdReader(env) {
36
+ const slot = findEnvSlot(env, "cwd");
37
+ if (slot === undefined)
38
+ return undefined;
39
+ const getter = slot.desc.get;
40
+ const initial = getter !== undefined ? reflectApply(getter, env, []) : slot.desc.value;
41
+ if (typeof initial !== "string")
42
+ return undefined;
43
+ let last = initial;
44
+ return () => {
45
+ const desc = getOwnPropertyDescriptor(slot.holder, "cwd");
46
+ const value = desc === undefined
47
+ ? undefined
48
+ : desc.get !== undefined
49
+ ? desc.get === getter
50
+ ? reflectApply(desc.get, env, [])
51
+ : undefined
52
+ : desc.value;
53
+ if (typeof value === "string")
54
+ last = value;
55
+ return last;
56
+ };
57
+ }
58
+ export function createHookEnvCapabilities(env) {
59
+ const face = Object.create(null);
60
+ const canonicalPath = readEnvPrimitive(env, "canonicalPath");
61
+ const exists = readEnvPrimitive(env, "exists");
62
+ const readLink = readEnvPrimitive(env, "readLink");
63
+ const cwd = createCwdReader(env);
64
+ if (typeof canonicalPath === "function") {
65
+ face.canonicalPath = (path, abortSignal) => reflectApply(canonicalPath, env, [path, abortSignal]);
66
+ }
67
+ if (typeof exists === "function") {
68
+ face.exists = (path, abortSignal) => reflectApply(exists, env, [path, abortSignal]);
69
+ }
70
+ if (typeof readLink === "function") {
71
+ face.readLink = (path, abortSignal) => reflectApply(readLink, env, [path, abortSignal]);
72
+ }
73
+ if (cwd !== undefined) {
74
+ face.cwd = cwd;
75
+ }
76
+ return Object.freeze(face);
77
+ }
17
78
  export function formatHookFeedback(text) {
18
79
  return `<system-reminder>\n${text}\n</system-reminder>`;
19
80
  }
@@ -61,6 +122,7 @@ function withProbeTimeout(p, ms, signal) {
61
122
  export async function runToolGate(input) {
62
123
  const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
63
124
  const { toolCallId, toolName } = event;
125
+ const hookCtx = () => ({ toolCallId, toolName, ...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}) });
64
126
  let currentInput = event.input;
65
127
  const preToolContext = [];
66
128
  let hookAsk;
@@ -68,7 +130,7 @@ export async function runToolGate(input) {
68
130
  if (preToolUse) {
69
131
  let r;
70
132
  try {
71
- r = screenPreToolUseResult(await preToolUse(toolName, currentInput, { toolCallId, toolName }));
133
+ r = screenPreToolUseResult(await preToolUse(toolName, currentInput, hookCtx()));
72
134
  }
73
135
  catch (err) {
74
136
  const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
@@ -147,7 +209,10 @@ export async function runToolGate(input) {
147
209
  currentInput = policyRewrite;
148
210
  req.args = policyRewrite;
149
211
  }
150
- if (input.autoMode && decision.action === "ask" && input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
212
+ if (input.autoMode &&
213
+ decision.action === "ask" &&
214
+ req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
215
+ input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
151
216
  const verdict = await input.autoMode.decider
152
217
  .decide({ req, askMessage: decisionText(decision) }, input.abortSignal)
153
218
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -180,13 +245,35 @@ export async function runToolGate(input) {
180
245
  }
181
246
  }
182
247
  if (decision.action === "ask" && req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
183
- decision = {
184
- action: "deny",
185
- message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
186
- "facility, or the suspend failed) and a synchronous permission approver cannot carry an answer — " +
187
- "the question was NOT shown to anyone. Proceed with your best judgment and state the assumption " +
188
- "you made.",
189
- };
248
+ const outcome = input.resolveContentAsk !== undefined ? await input.resolveContentAsk(req) : { kind: "unavailable", parkDeclined: false };
249
+ if (outcome.kind === "answered") {
250
+ decision = { action: "allow", updatedInput: outcome.presentedInput };
251
+ }
252
+ else if (outcome.kind === "unavailable") {
253
+ if (outcome.presentedInput !== undefined) {
254
+ currentInput = outcome.presentedInput;
255
+ req.args = outcome.presentedInput;
256
+ }
257
+ if (suspendAsk && outcome.parkDeclined) {
258
+ const suspended = await suspendAsk(req, currentInput, safety, true);
259
+ if (suspended) {
260
+ return { suspend: suspended, preToolContext };
261
+ }
262
+ }
263
+ decision = {
264
+ action: "deny",
265
+ message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
266
+ "facility, or the suspend failed) and a synchronous permission approver cannot carry an answer — " +
267
+ "the question was NOT shown to anyone. Proceed with your best judgment and state the assumption " +
268
+ "you made.",
269
+ };
270
+ }
271
+ else {
272
+ if (outcome.presentedInput !== undefined)
273
+ decision = { action: "allow", updatedInput: outcome.presentedInput };
274
+ else
275
+ decision = { action: "allow" };
276
+ }
190
277
  }
191
278
  if (decision.action === "ask") {
192
279
  const resolved = await resolveAsk(decision, req);
@@ -212,7 +299,7 @@ export async function runToolGate(input) {
212
299
  if (preToolUse) {
213
300
  let hr;
214
301
  try {
215
- hr = screenPreToolUseResult(await preToolUse(toolName, editArgs, { toolCallId, toolName }));
302
+ hr = screenPreToolUseResult(await preToolUse(toolName, editArgs, hookCtx()));
216
303
  }
217
304
  catch (err) {
218
305
  traceHookCrash(input, err, notifier);
@@ -0,0 +1,12 @@
1
+ import { type ActorAssertion } from "../internal/llm.js";
2
+ import type { HumanInputEvent, HumanInputSource, TaskEventIdentity } from "./types.js";
3
+ export type { HumanInputSource, HumanInputDelivery, HumanInputEvent } from "./types.js";
4
+ export interface HumanInputFrame {
5
+ text: string;
6
+ actor?: ActorAssertion;
7
+ source: HumanInputSource;
8
+ }
9
+ export declare function projectHumanInput(frame: HumanInputFrame): string;
10
+ export declare function buildHumanInputEvent(input: Omit<HumanInputEvent, "type" | "inputId" | keyof TaskEventIdentity> & {
11
+ inputId?: string;
12
+ }): HumanInputEvent;
@@ -0,0 +1,27 @@
1
+ import { snapshotActorAssertion } from "../internal/llm.js";
2
+ import { uuidv7 } from "../internal/harness.js";
3
+ import { inlineUntrusted } from "./untrusted-text.js";
4
+ import { attrEscape, EXTERNAL_SOURCE_MAX } from "./task-notification.js";
5
+ export function projectHumanInput(frame) {
6
+ if (frame.actor === undefined || frame.source === "system")
7
+ return frame.text;
8
+ if (frame.text.trim().length === 0)
9
+ return frame.text;
10
+ const label = attrEscape(inlineUntrusted(frame.actor.id, EXTERNAL_SOURCE_MAX));
11
+ const mark = frame.actor.hostAsserted ? "" : " (unverified)";
12
+ return `[from "${label}"${mark}]\n${frame.text}`;
13
+ }
14
+ export function buildHumanInputEvent(input) {
15
+ return {
16
+ type: "human_input",
17
+ inputId: input.inputId ?? uuidv7(),
18
+ sessionSeq: input.sessionSeq,
19
+ carrier: input.carrier,
20
+ source: input.source,
21
+ delivery: input.delivery,
22
+ ...(input.issuer !== undefined ? { issuer: input.issuer } : {}),
23
+ ...(input.actor !== undefined ? { actor: snapshotActorAssertion(input.actor) } : {}),
24
+ ...(input.principal !== undefined ? { principal: input.principal } : {}),
25
+ ...(input.entryId !== undefined ? { entryId: input.entryId } : {}),
26
+ };
27
+ }
@@ -0,0 +1,27 @@
1
+ import type { RunnerDeps, TaskSpec } from "./types.js";
2
+ import type { ToolPolicy } from "./tool-policy.js";
3
+ export type LockedKey = "mcp" | "toolPolicy" | "compliancePosture" | "retentionPolicy";
4
+ export declare const LOCKED_KEY_REGISTRY: {
5
+ readonly mcp: {
6
+ readonly specFields: readonly ["mcp"];
7
+ };
8
+ readonly toolPolicy: {
9
+ readonly specFields: readonly ["toolPolicy"];
10
+ };
11
+ readonly compliancePosture: {
12
+ readonly specFields: readonly [];
13
+ };
14
+ readonly retentionPolicy: {
15
+ readonly specFields: readonly [];
16
+ };
17
+ };
18
+ export interface LockedConfig {
19
+ keys: readonly LockedKey[];
20
+ }
21
+ export interface LockedPreflight {
22
+ lockedKeys: ReadonlySet<LockedKey>;
23
+ mcp: TaskSpec["mcp"];
24
+ toolPolicy: ToolPolicy | undefined;
25
+ }
26
+ export declare function resolveLockedKeys(config: LockedConfig | undefined): ReadonlySet<LockedKey>;
27
+ export declare function preflightLockedConfig(spec: TaskSpec, deps: RunnerDeps): LockedPreflight;
@@ -0,0 +1,42 @@
1
+ export const LOCKED_KEY_REGISTRY = {
2
+ mcp: { specFields: ["mcp"] },
3
+ toolPolicy: { specFields: ["toolPolicy"] },
4
+ compliancePosture: { specFields: [] },
5
+ retentionPolicy: { specFields: [] },
6
+ };
7
+ function codedError(code, message) {
8
+ const e = new Error(message);
9
+ e.code = code;
10
+ return e;
11
+ }
12
+ export function resolveLockedKeys(config) {
13
+ if (config === undefined)
14
+ return new Set();
15
+ const keys = new Set();
16
+ for (const key of config.keys) {
17
+ if (!Object.prototype.hasOwnProperty.call(LOCKED_KEY_REGISTRY, key)) {
18
+ throw codedError("config.locked_registry", `lockedConfig.keys contains "${String(key)}", which is not a member of the locked-key registry ` +
19
+ `(${Object.keys(LOCKED_KEY_REGISTRY).join(", ")}). A lock that does not parse must not half-arm — ` +
20
+ `fix the deployment's lock declaration.`);
21
+ }
22
+ keys.add(key);
23
+ }
24
+ return keys;
25
+ }
26
+ export function preflightLockedConfig(spec, deps) {
27
+ const lockedKeys = resolveLockedKeys(deps.lockedConfig);
28
+ for (const key of lockedKeys) {
29
+ for (const field of LOCKED_KEY_REGISTRY[key].specFields) {
30
+ if (spec[field] !== undefined) {
31
+ throw codedError("config.locked_key", `TaskSpec.${field} is administratively locked by this deployment (locked key "${key}") — ` +
32
+ `the task-supplied value is refused wholesale (locks are config-time; there is no request-time ` +
33
+ `unlock). Remove the field from the task, or change the deployment's lock configuration.`);
34
+ }
35
+ }
36
+ }
37
+ return {
38
+ lockedKeys,
39
+ mcp: lockedKeys.has("mcp") ? undefined : spec.mcp,
40
+ toolPolicy: lockedKeys.has("toolPolicy") ? deps.toolPolicy : (spec.toolPolicy ?? deps.toolPolicy),
41
+ };
42
+ }
@@ -5,8 +5,8 @@ import { type McpImageResizer } from "./image-downsample.js";
5
5
  import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js";
6
6
  export interface McpToolAxis {
7
7
  name: string;
8
- irreversibility?: "always";
9
- egress?: true;
8
+ irreversibility?: "always" | "never";
9
+ egress?: boolean;
10
10
  effect?: ToolEffect;
11
11
  }
12
12
  export interface McpDroppedTool {
@@ -114,6 +114,11 @@ export type McpSchemaNormalizeResult = {
114
114
  export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormalizeResult;
115
115
  export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
116
116
  export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer): Promise<MaterializedMcp>;
117
+ export declare function applyCallerAxisOverride(name: string, hint: McpToolAxis | undefined, override: {
118
+ effect?: ToolEffect;
119
+ egress?: boolean;
120
+ irreversibility?: "always" | "never";
121
+ } | undefined): McpToolAxis | undefined;
117
122
  export declare function classifyDirReadInvalidParams(message: string): "not_found" | "not_directory";
118
123
  export declare function parseCallToolResultLenient(data: unknown): {
119
124
  success: true;
package/dist/core/mcp.js CHANGED
@@ -720,7 +720,7 @@ function mcpAxisFor(namespacedName, annotations) {
720
720
  }
721
721
  return any ? axis : undefined;
722
722
  }
723
- function applyCallerAxisOverride(name, hint, override) {
723
+ export function applyCallerAxisOverride(name, hint, override) {
724
724
  if (!override)
725
725
  return hint;
726
726
  let effect = hint?.effect;
@@ -729,16 +729,16 @@ function applyCallerAxisOverride(name, hint, override) {
729
729
  if (override.effect !== undefined)
730
730
  effect = override.effect;
731
731
  if (override.egress !== undefined)
732
- egress = override.egress ? true : undefined;
732
+ egress = override.egress;
733
733
  if (override.irreversibility !== undefined)
734
- irrev = override.irreversibility === "always" ? "always" : undefined;
734
+ irrev = override.irreversibility;
735
735
  const axis = { name };
736
736
  if (effect !== undefined)
737
737
  axis.effect = effect;
738
- if (egress)
739
- axis.egress = true;
740
- if (irrev)
741
- axis.irreversibility = "always";
738
+ if (egress !== undefined)
739
+ axis.egress = egress;
740
+ if (irrev !== undefined)
741
+ axis.irreversibility = irrev;
742
742
  return axis.effect !== undefined || axis.egress !== undefined || axis.irreversibility !== undefined ? axis : undefined;
743
743
  }
744
744
  const LIST_MCP_RESOURCES = "ListMcpResourcesTool";
@@ -0,0 +1,51 @@
1
+ import type { NormalizedMemorySpec } from "./memory.js";
2
+ export type MemoryScopeOrigin = "deployment" | "request";
3
+ export interface MemoryScopeRequest {
4
+ scope: string;
5
+ origin: MemoryScopeOrigin;
6
+ }
7
+ export type MemoryAdmissionVerdict = {
8
+ ok: true;
9
+ scopes: readonly string[];
10
+ writeScope: string | null;
11
+ } | {
12
+ ok: false;
13
+ reason: string;
14
+ };
15
+ export type MemoryScopeAdmission = (input: {
16
+ principal: string | undefined;
17
+ requested: ReadonlyArray<MemoryScopeRequest>;
18
+ requestedWriteScope: MemoryScopeRequest | null;
19
+ }) => MemoryAdmissionVerdict | Promise<MemoryAdmissionVerdict>;
20
+ export interface MemoryAdmissionInput {
21
+ memorySpec: NormalizedMemorySpec;
22
+ principal: string | undefined;
23
+ admission: MemoryScopeAdmission | undefined;
24
+ deploymentScopes: ReadonlySet<string>;
25
+ orgMemoryDenied: boolean;
26
+ complianceDegraded: boolean;
27
+ parentAdmittedOrgScopes: readonly string[] | undefined;
28
+ priorOwnVerdict: OwnOrgAdmissionVerdict | undefined;
29
+ governedProvenance: boolean;
30
+ }
31
+ export interface MemoryAdmissionOutcome {
32
+ scopes: string[];
33
+ writeScope: string | null;
34
+ admittedOrgScopes: string[];
35
+ droppedDeploymentScopes: string[];
36
+ writeScopeNarrowed: boolean;
37
+ ownVerdict: OwnOrgAdmissionVerdict | undefined;
38
+ }
39
+ export declare function foldAdmissionFreeze(input: {
40
+ delegated: boolean;
41
+ inherited: readonly string[] | undefined;
42
+ }): readonly string[] | undefined;
43
+ export interface OwnOrgAdmissionVerdict {
44
+ scopes: readonly string[];
45
+ writeScope: string | null;
46
+ }
47
+ export declare function readDurableOrgAdmission(row: {
48
+ admittedOrgScopes?: readonly string[];
49
+ admittedOrgWriteScope?: string | null;
50
+ }): OwnOrgAdmissionVerdict;
51
+ export declare function admitMemoryScopes(input: MemoryAdmissionInput): Promise<MemoryAdmissionOutcome>;
@@ -0,0 +1,159 @@
1
+ import { parseScopeKey } from "./memory-engine/scope-contract.js";
2
+ function codedError(code, message) {
3
+ const e = new Error(message);
4
+ e.code = code;
5
+ return e;
6
+ }
7
+ export function foldAdmissionFreeze(input) {
8
+ return input.delegated ? (input.inherited ?? []) : input.inherited;
9
+ }
10
+ export function readDurableOrgAdmission(row) {
11
+ return { scopes: row.admittedOrgScopes ?? [], writeScope: row.admittedOrgWriteScope ?? null };
12
+ }
13
+ const ORG_SHAPE = /^org:/;
14
+ function orgScopesOf(spec) {
15
+ if (spec.scopeContract !== "v2")
16
+ return { readOrg: [], writeOrg: null };
17
+ const readOrg = spec.scopes.filter((s) => parseScopeKey(s).kind === "org");
18
+ const writeOrg = spec.writeScope !== null && parseScopeKey(spec.writeScope).kind === "org" ? spec.writeScope : null;
19
+ return { readOrg, writeOrg };
20
+ }
21
+ export async function admitMemoryScopes(input) {
22
+ const { memorySpec, principal, admission, deploymentScopes, parentAdmittedOrgScopes } = input;
23
+ const governed = admission !== undefined ||
24
+ deploymentScopes.size > 0 ||
25
+ input.priorOwnVerdict !== undefined ||
26
+ input.governedProvenance ||
27
+ input.orgMemoryDenied ||
28
+ (parentAdmittedOrgScopes !== undefined && parentAdmittedOrgScopes.length > 0);
29
+ if (governed && memorySpec.scopeContract !== "v2") {
30
+ const shaped = [...memorySpec.scopes, ...(memorySpec.writeScope !== null ? [memorySpec.writeScope] : [])].find((s) => ORG_SHAPE.test(s));
31
+ if (shaped !== undefined) {
32
+ throw codedError("memory.admission_denied", `memory scope "${shaped}" is org-shaped but the task did not opt into the v2 scope contract ` +
33
+ `(memory.scopeContract: "v2") — on a deployment with org-memory admission configured, org scopes ` +
34
+ `outside the contract are refused (fail-closed), not mounted as opaque keys.`);
35
+ }
36
+ }
37
+ const { readOrg, writeOrg } = orgScopesOf(memorySpec);
38
+ const allOrg = [...readOrg, ...(writeOrg !== null && !readOrg.includes(writeOrg) ? [writeOrg] : [])];
39
+ if (allOrg.length === 0) {
40
+ return {
41
+ scopes: [...memorySpec.scopes],
42
+ writeScope: memorySpec.writeScope,
43
+ admittedOrgScopes: [],
44
+ droppedDeploymentScopes: [],
45
+ writeScopeNarrowed: false,
46
+ ownVerdict: undefined,
47
+ };
48
+ }
49
+ const priorOwn = input.priorOwnVerdict;
50
+ const applySessionFreeze = (proj) => {
51
+ let { admitted, scopes, writeScope, dropped, writeScopeNarrowed } = proj;
52
+ if (priorOwn !== undefined) {
53
+ const outsideRequest = admitted.filter((s) => !deploymentScopes.has(s) && !priorOwn.scopes.includes(s));
54
+ if (outsideRequest.length > 0) {
55
+ throw codedError("memory.admission_denied", `org memory scope(s) ${outsideRequest.map((s) => `"${s}"`).join(", ")} are outside this session's frozen ` +
56
+ `admission verdict (recorded at suspend) — a session's org grant can only narrow across suspend/resume.`);
57
+ }
58
+ const frozenOut = admitted.filter((s) => !priorOwn.scopes.includes(s));
59
+ if (frozenOut.length > 0) {
60
+ dropped = [...dropped, ...frozenOut];
61
+ admitted = admitted.filter((s) => priorOwn.scopes.includes(s));
62
+ scopes = scopes.filter((s) => !frozenOut.includes(s));
63
+ }
64
+ if (writeScope !== null && writeOrg !== null && writeScope === writeOrg && priorOwn.writeScope !== writeScope) {
65
+ writeScope = null;
66
+ writeScopeNarrowed = true;
67
+ }
68
+ }
69
+ return {
70
+ scopes,
71
+ writeScope,
72
+ admittedOrgScopes: admitted,
73
+ droppedDeploymentScopes: dropped,
74
+ writeScopeNarrowed,
75
+ ownVerdict: { scopes: admitted, writeScope: writeScope !== null && writeOrg !== null && writeScope === writeOrg ? writeScope : null },
76
+ };
77
+ };
78
+ const stampedOrigins = memorySpec.scopeOrigins;
79
+ const originOf = (scope) => stampedOrigins !== undefined
80
+ ? (stampedOrigins[scope] === "deployment" ? "deployment" : "request")
81
+ : deploymentScopes.has(scope)
82
+ ? "deployment"
83
+ : "request";
84
+ const requests = allOrg.map((scope) => ({ scope, origin: originOf(scope) }));
85
+ const requestOrigin = requests.filter((r) => r.origin === "request");
86
+ if (input.orgMemoryDenied && requestOrigin.length > 0) {
87
+ throw codedError(input.complianceDegraded ? "config.compliance_required" : "config.compliance_denied", `org memory scope(s) ${requestOrigin.map((r) => `"${r.scope}"`).join(", ")} are denied for this principal by the ` +
88
+ `compliance posture (capability "org_memory_mount"${input.complianceDegraded ? "; posture resolver failing, fail-closed" : ""}).`);
89
+ }
90
+ if (parentAdmittedOrgScopes !== undefined) {
91
+ const exceeding = requestOrigin.filter((r) => !parentAdmittedOrgScopes.includes(r.scope));
92
+ if (exceeding.length > 0) {
93
+ throw codedError("memory.admission_denied", `delegated child requests org memory scope(s) ${exceeding.map((r) => `"${r.scope}"`).join(", ")} outside the ` +
94
+ `parent's frozen admission verdict — a child plane can only narrow the parent's org grant, never exceed it.`);
95
+ }
96
+ }
97
+ if (admission === undefined) {
98
+ if (requestOrigin.length > 0) {
99
+ throw codedError("memory.admission_required", `org memory scope(s) ${requestOrigin.map((r) => `"${r.scope}"`).join(", ")} come from the request side but this ` +
100
+ `deployment has no memory-scope admission resolver (RunnerDeps.memoryScopeAdmission) — refused fail-closed. ` +
101
+ `Deployment-pinned org scopes belong in RunnerDeps.deploymentMemoryScopes.`);
102
+ }
103
+ return applySessionFreeze({
104
+ admitted: allOrg,
105
+ scopes: [...memorySpec.scopes],
106
+ writeScope: memorySpec.writeScope,
107
+ dropped: [],
108
+ writeScopeNarrowed: false,
109
+ });
110
+ }
111
+ if (requestOrigin.length > 0 && principal === undefined) {
112
+ throw codedError("memory.admission_denied", `org memory scope(s) ${requestOrigin.map((r) => `"${r.scope}"`).join(", ")} require an authenticated ` +
113
+ `TaskSpec.principal for admission — a principal-less request cannot be admitted to a tenant plane.`);
114
+ }
115
+ let verdict;
116
+ try {
117
+ verdict = await admission({
118
+ principal,
119
+ requested: requests,
120
+ requestedWriteScope: writeOrg !== null ? { scope: writeOrg, origin: originOf(writeOrg) } : null,
121
+ });
122
+ }
123
+ catch (err) {
124
+ const hinted = err.retryAfterMs;
125
+ const e = codedError("memory.admission_required", `memory-scope admission resolver failed (${err instanceof Error ? err.message : String(err)}) — refused fail-closed.`);
126
+ if (typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0)
127
+ e.retryAfterMs = hinted;
128
+ throw e;
129
+ }
130
+ if (!verdict.ok) {
131
+ throw codedError("memory.admission_denied", `org memory admission denied: ${verdict.reason}`);
132
+ }
133
+ const unrequested = verdict.scopes.filter((s) => !allOrg.includes(s));
134
+ if (unrequested.length > 0 || (verdict.writeScope !== null && verdict.writeScope !== writeOrg)) {
135
+ throw codedError("memory.admission_denied", `memory-scope admission verdict granted unrequested scope(s) ` +
136
+ `${[...unrequested, ...(verdict.writeScope !== null && verdict.writeScope !== writeOrg ? [String(verdict.writeScope)] : [])].map((s) => `"${s}"`).join(", ")} — ` +
137
+ `admission can only narrow a request, never extend it; treating the verdict as a resolver fault.`);
138
+ }
139
+ const refusedRequest = requestOrigin.filter((r) => !verdict.scopes.includes(r.scope));
140
+ if (refusedRequest.length > 0) {
141
+ throw codedError("memory.admission_denied", `org memory scope(s) ${refusedRequest.map((r) => `"${r.scope}"`).join(", ")} were not admitted — the whole ` +
142
+ `prepare is refused (two-state verdict; partial admission is never applied silently).`);
143
+ }
144
+ const dropped = requests.filter((r) => r.origin === "deployment" && !verdict.scopes.includes(r.scope)).map((r) => r.scope);
145
+ const admitted = allOrg.filter((s) => !dropped.includes(s));
146
+ const scopes = memorySpec.scopes.filter((s) => !dropped.includes(s));
147
+ let writeScope = memorySpec.writeScope;
148
+ let writeScopeNarrowed = false;
149
+ if (writeOrg !== null) {
150
+ if (verdict.writeScope === writeOrg && !dropped.includes(writeOrg)) {
151
+ writeScope = writeOrg;
152
+ }
153
+ else {
154
+ writeScope = null;
155
+ writeScopeNarrowed = true;
156
+ }
157
+ }
158
+ return applySessionFreeze({ admitted, scopes, writeScope, dropped, writeScopeNarrowed });
159
+ }
@@ -94,12 +94,14 @@ export interface NormalizedMemorySpec {
94
94
  writeScope: string | null;
95
95
  enabled: boolean;
96
96
  scopeContract?: "v2";
97
+ scopeOrigins?: Readonly<Record<string, "deployment" | "request">>;
97
98
  }
98
99
  export type MemorySpecInput = {
99
100
  scopes?: ReadonlyArray<string>;
100
101
  writeScope?: string | null;
101
102
  enabled?: boolean;
102
103
  scopeContract?: "v2";
104
+ scopeOrigins?: Readonly<Record<string, "deployment" | "request">>;
103
105
  };
104
106
  export declare function normalizeMemorySpec(input: MemorySpecInput | undefined): NormalizedMemorySpec | undefined;
105
107
  export declare const MAX_MEMORY_BYTES: number;
@@ -384,12 +384,13 @@ export function normalizeMemorySpec(input) {
384
384
  else {
385
385
  writeScope = scopes[scopes.length - 1];
386
386
  }
387
+ const origins = input.scopeOrigins !== undefined ? { scopeOrigins: input.scopeOrigins } : {};
387
388
  if (input.scopeContract === "v2") {
388
389
  for (const key of writeScope !== null ? [...scopes, writeScope] : scopes)
389
390
  parseScopeKey(key);
390
- return { scopes, writeScope, enabled: input.enabled !== false, scopeContract: "v2" };
391
+ return { scopes, writeScope, enabled: input.enabled !== false, scopeContract: "v2", ...origins };
391
392
  }
392
- return { scopes, writeScope, enabled: input.enabled !== false };
393
+ return { scopes, writeScope, enabled: input.enabled !== false, ...origins };
393
394
  }
394
395
  export const MAX_MEMORY_BYTES = 100 * 1024;
395
396
  export const MEMORY_WRAPPER_TAGS = ["user_memory"];