@deepstrike/wasm 0.2.49 → 0.2.50

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.
@@ -209,11 +209,52 @@ export interface RuntimeOptions {
209
209
  requiredEvidence: string[];
210
210
  }) => Promise<MilestoneCheckResult> | MilestoneCheckResult;
211
211
  runSpec?: AgentRunSpec;
212
- /** P0-A tool gating: a static per-run tool profile — only these tool ids (plus the meta-tools)
213
- * are exposed to the model each turn. Lowers to the same `capability_filter` sub-agents use;
214
- * byte-stable across the run, so it never busts the prompt-cache prefix. Augments `runSpec`'s
215
- * filter when both set; synthesizes a minimal spec otherwise. Omitted/empty no gating. */
212
+ /**
213
+ * The run's **exposure ceiling** — the outer bound on what this run may EVER advertise to the
214
+ * model. Not a static profile: it is an INTERSECTION applied on every turn (`exposed ⊆ ceiling`),
215
+ * so every narrowing mechanism operates *within* it and none can widen past it. Skills narrow
216
+ * inside the ceiling (`allowed_tools`), `baselineToolIds` selects which of the ceiling's tools are
217
+ * exposed before any skill activates, and `stableCoreToolIds` pins tools against skill narrowing.
218
+ *
219
+ * Exempt on the id axis: the kernel-owned meta-tools (`skill`, `memory`, `knowledge`,
220
+ * `update_plan`, `read_result`) stay exposed regardless of this list — a ceiling that hid `skill`
221
+ * would make progressive disclosure unreachable. The KIND axis
222
+ * (`runSpec.capabilityFilter.allowedKinds`) still applies to them.
223
+ *
224
+ * Byte-stable across the run, so it never busts the prompt-cache prefix. Lowers to the same
225
+ * `capability_filter` sub-agents use: augments `runSpec`'s filter when both are set, else
226
+ * synthesizes a minimal run spec. Omitted **or empty** ⇒ no ceiling (all registered tools) — the
227
+ * empty array is NOT a minimal surface here; use `baselineToolIds: []` for that.
228
+ *
229
+ * Enforcement: `toolDispatchGate` (default `"exposed"`) makes this a real boundary — a call to a
230
+ * tool outside the advertised set never executes.
231
+ */
216
232
  allowedToolIds?: string[];
233
+ /**
234
+ * The **pre-activation** exposure surface, selected from under the `allowedToolIds` ceiling.
235
+ * Makes the narrow→wide progressive-disclosure shape expressible: start the run advertising only
236
+ * these tools, and let a skill activation widen the surface by exactly its declared
237
+ * `allowed_tools` (still ∩ the ceiling). Per turn:
238
+ *
239
+ * `exposed = meta ∪ ((baseline ∪ stableCore ∪ ⋃ activeSkills.allowed_tools) ∩ ceiling)`
240
+ *
241
+ * An active skill that declares no `allowed_tools` contributes nothing — with a baseline set the
242
+ * surface stays narrow (strict; the legacy errs-open widening is deliberately not inherited).
243
+ *
244
+ * `undefined` ⇒ legacy behavior, byte-identical. `[]` is a legitimate, distinct value: the minimal
245
+ * surface (meta-tools + `stableCoreToolIds` only) — the `allowedToolIds` "empty means no gating"
246
+ * trap does NOT recur here. Entries outside the ceiling silently intersect away.
247
+ */
248
+ baselineToolIds?: string[];
249
+ /**
250
+ * Dispatch enforcement for the exposure surface. `"exposed"` (default) is fail-closed: a tool call
251
+ * the model was never advertised this turn never reaches the host — the kernel commits a
252
+ * model-visible `governance_denied` result instead, which feeds the repeat fuse like any other
253
+ * denial. Allowed siblings in the same batch still execute; `pace` and the meta-tool family always
254
+ * pass through. `"registered"` is the escape hatch restoring the pre-gate permissive behavior (any
255
+ * registered tool the model names executes, even if it was gated out of the tools schema).
256
+ */
257
+ toolDispatchGate?: "exposed" | "registered";
217
258
  /** P0-C: optional per-turn metrics sink for tool-gating telemetry (see `TurnMetrics`). Pure
218
259
  * observation; invoked once per LLM turn. Never throws into the run loop (errors are swallowed). */
219
260
  onTurnMetrics?: (metrics: TurnMetrics) => void;
@@ -539,20 +539,26 @@ export class RuntimeRunner {
539
539
  kind: "start_run",
540
540
  task: { goal, criteria },
541
541
  };
542
- // P0-A: lower an explicit `runSpec` and/or the `allowedToolIds` profile to the kernel's
543
- // `capability_filter` (reuses the existing run_spec wire — no new ABI). Unset on both ⇒ no
544
- // gating (铁律: no config = old behavior).
542
+ // P0-A: lower an explicit `runSpec`, the `allowedToolIds` ceiling, and/or the `baselineToolIds`
543
+ // pre-activation surface to the kernel run spec (reuses the existing run_spec wire — no new
544
+ // ABI). Unset on all ⇒ no gating (铁律: no config = old behavior).
545
545
  const allowedToolIds = this.opts.allowedToolIds;
546
546
  const hasProfile = allowedToolIds !== undefined && allowedToolIds.length > 0;
547
- if (this.opts.runSpec || hasProfile) {
547
+ // NOT the `length > 0` idiom above: `baselineToolIds: []` is the legitimate minimal surface
548
+ // (meta + stable-core only), so mere presence triggers the lowering.
549
+ const baselineToolIds = this.opts.baselineToolIds;
550
+ const hasBaseline = baselineToolIds !== undefined;
551
+ if (this.opts.runSpec || hasProfile || hasBaseline) {
548
552
  const baseSpec = this.opts.runSpec ?? {
549
553
  identity: { agentId: this.opts.agentId ?? "root", sessionId, isSubAgent: false },
550
554
  role: "custom",
551
555
  goal,
552
556
  };
553
- const spec = hasProfile
557
+ let spec = hasProfile
554
558
  ? { ...baseSpec, capabilityFilter: { ...baseSpec.capabilityFilter, allowedIds: allowedToolIds } }
555
559
  : baseSpec;
560
+ if (hasBaseline)
561
+ spec = { ...spec, exposureBaseline: baselineToolIds };
556
562
  startPayload.run_spec = agentRunSpecToKernel(spec);
557
563
  }
558
564
  this.applyKernelPolicies(runtime);
@@ -1458,6 +1464,11 @@ export class RuntimeRunner {
1458
1464
  if (this.opts.criteriaGate !== undefined) {
1459
1465
  config.criteria_gate = this.opts.criteriaGate;
1460
1466
  }
1467
+ // P1: fail-closed dispatch selector (absent ⇒ kernel default "exposed"). "registered" is the
1468
+ // escape hatch back to permissive dispatch; the kernel rejects any other value.
1469
+ if (this.opts.toolDispatchGate !== undefined) {
1470
+ config.tool_dispatch_gate = this.opts.toolDispatchGate;
1471
+ }
1461
1472
  // K2: knowledge budget ratio (absent ⇒ kernel default 0.25; 0 disables).
1462
1473
  if (this.opts.knowledgeBudgetRatio !== undefined) {
1463
1474
  config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
@@ -41,6 +41,14 @@ export interface AgentRunSpec {
41
41
  /** ③ loop-agent rounds: presence makes this run ONE round of a paced loop (gates the
42
42
  * kernel `pace` meta-tool and arms the pacing trap). */
43
43
  loopRound?: LoopRoundSpec;
44
+ /** Exposure baseline — the PRE-ACTIVATION tool surface *under* the `capabilityFilter` ceiling.
45
+ * The ceiling bounds what this run may EVER expose; the baseline selects which of those are
46
+ * advertised before any skill activates, so `exposed = meta ∪ ((baseline ∪ stableCore ∪
47
+ * ⋃ activeSkills.allowed_tools) ∩ ceiling)`. That makes narrow→wide progressive disclosure
48
+ * expressible. Absent ⇒ legacy behavior. `[]` is meaningful and distinct from absent: the minimal
49
+ * surface (meta-tools + stable-core only). Entries outside the ceiling silently intersect away.
50
+ * Lowered from `RuntimeOptions.baselineToolIds`. */
51
+ exposureBaseline?: string[];
44
52
  /** M1/G3: per-agent model preference; the host resolves it via `RuntimeOptions.providerFor`.
45
53
  * Host-side routing only — not sent to the kernel. */
46
54
  modelHint?: string;
@@ -54,6 +54,11 @@ export function agentRunSpecToKernel(spec) {
54
54
  ...(spec.loopRound.defaultAction !== undefined ? { default_action: spec.loopRound.defaultAction } : {}),
55
55
  };
56
56
  }
57
+ // Exposure baseline: `undefined` ⇒ omit the field entirely (kernel `None` = legacy behavior);
58
+ // `[]` ⇒ send `[]` (kernel `Some([])` = the minimal surface). The unset/minimal distinction is
59
+ // load-bearing, so this is deliberately NOT the `length > 0` idiom `allowedToolIds` uses.
60
+ if (spec.exposureBaseline !== undefined)
61
+ out.exposure_baseline = [...spec.exposureBaseline];
57
62
  return out;
58
63
  }
59
64
  export function milestoneContractToKernel(contract) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/wasm",
3
- "version": "0.2.49",
3
+ "version": "0.2.50",
4
4
  "description": "DeepStrike WASM SDK — browser, Cloudflare Workers, Deno Deploy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "test": "node --experimental-vm-modules node_modules/.bin/jest"
16
16
  },
17
17
  "dependencies": {
18
- "@deepstrike/wasm-kernel": "0.2.49"
18
+ "@deepstrike/wasm-kernel": "0.2.50"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/jest": "^30.0.0",