@deepstrike/sdk 0.2.48 → 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.
- package/dist/harness/manifest.d.ts +11 -10
- package/dist/harness/manifest.js +51 -36
- package/dist/harness/nudge.d.ts +1 -1
- package/dist/harness/public.js +1 -1
- package/dist/runtime/filtered-plane.js +4 -1
- package/dist/runtime/runner.d.ts +52 -8
- package/dist/runtime/runner.js +18 -6
- package/dist/types/agent.d.ts +9 -0
- package/dist/types/agent.js +5 -0
- package/package.json +2 -2
|
@@ -22,7 +22,7 @@ export declare function composeSystemPrompt(base: string | undefined, instructio
|
|
|
22
22
|
* The exact `RuntimeOptions` fields a manifest may drive. Derived via `Pick` so field names and types
|
|
23
23
|
* track `RuntimeOptions` verbatim; anything outside this set is rejected by `applyManifest`/`applyPatch`.
|
|
24
24
|
*/
|
|
25
|
-
export type HarnessRuntimePatch = Pick<RuntimeOptions, "maxTurns" | "maxTotalTokens" | "criteriaGate" | "repeatFuse" | "entropyWatch" | "knowledgeBudgetRatio" | "skillLeaseTurns" | "allowedToolIds" | "stableCoreToolIds" | "enablePlanTool" | "skillFilter"> & Pick<MemoryPolicy, "retrievalTopK" | "promotionRecallThreshold">;
|
|
25
|
+
export type HarnessRuntimePatch = Pick<RuntimeOptions, "maxTurns" | "maxTotalTokens" | "criteriaGate" | "repeatFuse" | "entropyWatch" | "knowledgeBudgetRatio" | "skillLeaseTurns" | "allowedToolIds" | "baselineToolIds" | "stableCoreToolIds" | "enablePlanTool" | "skillFilter"> & Pick<MemoryPolicy, "retrievalTopK" | "promotionRecallThreshold">;
|
|
26
26
|
/**
|
|
27
27
|
* The promotion tier of an editable surface — the SECOND axis of the safety boundary (the whitelist is
|
|
28
28
|
* the first). Even a whitelisted surface may need a heavier gate than "typed validation passed".
|
|
@@ -30,17 +30,18 @@ export type HarnessRuntimePatch = Pick<RuntimeOptions, "maxTurns" | "maxTotalTok
|
|
|
30
30
|
export type SurfaceTier = "auto" | "screened" | "human";
|
|
31
31
|
/**
|
|
32
32
|
* Map an editable surface to its promotion tier. Maintained HERE beside the whitelist so a surface can
|
|
33
|
-
* never be whitelisted without also being assigned a tier (spec
|
|
33
|
+
* never be whitelisted without also being assigned a tier (the spec's same-place maintenance rule):
|
|
34
34
|
* - "auto" (Tier A): every `runtime.*` whitelist surface. Typed validation + the capability
|
|
35
|
-
* ceiling invariant + the
|
|
35
|
+
* ceiling invariant + the acceptance rule already guard them, so promotion is fully
|
|
36
36
|
* automatic — there is no free text and no injection surface.
|
|
37
37
|
* - "screened" (Tier B): `instructions.*` and `nudges`. Free text can smuggle instructions
|
|
38
38
|
* (persistent prompt-injection laundered through the evidence loop), so a screen runs
|
|
39
39
|
* before promotion.
|
|
40
|
-
* - "human" (Tier C): reserved for capability-WIDENING surfaces. None exist
|
|
41
|
-
* semantics make widening structurally inexpressible — but the enum value exists so a
|
|
42
|
-
* surface cannot be added without consciously assigning it a tier (and
|
|
43
|
-
* human gate). `surfaceTier` therefore never returns "human"
|
|
40
|
+
* - "human" (Tier C): reserved for capability-WIDENING surfaces. None currently exist — intersection
|
|
41
|
+
* semantics make widening structurally inexpressible — but the enum value exists so a new
|
|
42
|
+
* capability-widening surface cannot be added without consciously assigning it a tier (and
|
|
43
|
+
* building the human gate). `surfaceTier` therefore never returns "human" today — no
|
|
44
|
+
* capability-widening surface is expressible.
|
|
44
45
|
* An unknown surface / slot / runtime key THROWS (same discipline as applySurfaceEdit): a surface with
|
|
45
46
|
* no tier must never fall through to auto-promotion.
|
|
46
47
|
*/
|
|
@@ -55,7 +56,7 @@ export interface HarnessManifest {
|
|
|
55
56
|
* Opaque isolation key — host decides its semantics (user / tenant / agent-group). Orthogonal to
|
|
56
57
|
* `modelProfile` (never concatenate the two — that reprises the identity-scoping bug class); absent
|
|
57
58
|
* ⇒ the host treats it as `"default"`. It rides canonical JSON, so digests domain-separate by scope,
|
|
58
|
-
* but an absent scope leaves a
|
|
59
|
+
* but an absent scope leaves a pre-scope manifest's digest byte-identical (canonicalJson skips
|
|
59
60
|
* undefined). Becomes a lineage directory name downstream, hence the path-safe character bound.
|
|
60
61
|
*/
|
|
61
62
|
scope?: string;
|
|
@@ -71,9 +72,9 @@ export interface HarnessManifest {
|
|
|
71
72
|
rationale?: string;
|
|
72
73
|
deltaHeldIn?: number;
|
|
73
74
|
deltaHeldOut?: number;
|
|
74
|
-
/** Promotion tier of the driving edit
|
|
75
|
+
/** Promotion tier of the driving edit. */
|
|
75
76
|
tier?: SurfaceTier;
|
|
76
|
-
/** Injection-screen verdict — present only for a screened (Tier B) promotion
|
|
77
|
+
/** Injection-screen verdict — present only for a screened (Tier B) promotion. */
|
|
77
78
|
screenVerdict?: "pass" | "screened_out";
|
|
78
79
|
};
|
|
79
80
|
}
|
package/dist/harness/manifest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-Harness
|
|
2
|
+
* Self-Harness editable surfaces — the harness face as DATA.
|
|
3
3
|
*
|
|
4
4
|
* A `HarnessManifest` is a versioned, hashable lineage node: the editable surfaces a fixed model may
|
|
5
5
|
* rewrite about its OWN harness — instruction slots, nudge rules, and a whitelisted `RuntimeOptions`
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* absent, so a proposer can never rewrite them (spec design principle: conservative promotion).
|
|
12
12
|
*
|
|
13
13
|
* Tool/skill surfaces add the SECOND safety invariant (spec design principle A — the capability
|
|
14
|
-
* ceiling): `allowedToolIds`, `stableCoreToolIds`, and `skillFilter` fold onto the
|
|
15
|
-
* INTERSECTION, never assignment. A manifest can only NARROW the tools/skills the
|
|
16
|
-
* exposes — never widen. Capability expansion (naming a tool the host does not expose) is
|
|
17
|
-
* structurally inexpressible, and the whole security audit stays O(1): read the whitelist,
|
|
18
|
-
* one invariant. (`enablePlanTool` is exempt — it toggles a kernel-owned meta-tool,
|
|
19
|
-
* not capability-granting, so it folds by plain assignment.)
|
|
14
|
+
* ceiling): `allowedToolIds`, `baselineToolIds`, `stableCoreToolIds`, and `skillFilter` fold onto the
|
|
15
|
+
* host baseline by INTERSECTION, never assignment. A manifest can only NARROW the tools/skills the
|
|
16
|
+
* host already exposes — never widen. Capability expansion (naming a tool the host does not expose) is
|
|
17
|
+
* therefore structurally inexpressible, and the whole security audit stays O(1): read the whitelist,
|
|
18
|
+
* check the one invariant. (`enablePlanTool` is exempt — it toggles a kernel-owned meta-tool,
|
|
19
|
+
* attention-shaping not capability-granting, so it folds by plain assignment.)
|
|
20
|
+
*
|
|
21
|
+
* `toolDispatchGate` is deliberately ABSENT from the whitelist and must stay so: it selects whether
|
|
22
|
+
* the kernel enforces the exposure surface at dispatch. A proposer that could set it to `"registered"`
|
|
23
|
+
* would disable the enforcement half of the ceiling it is otherwise structurally unable to widen.
|
|
20
24
|
*/
|
|
21
25
|
import { createHash } from "node:crypto";
|
|
22
26
|
import { validateNudgeRules } from "./nudge.js";
|
|
@@ -44,7 +48,12 @@ export function composeSystemPrompt(base, instructions) {
|
|
|
44
48
|
}
|
|
45
49
|
const MEMORY_POLICY_PATCH_KEYS = ["retrievalTopK", "promotionRecallThreshold"];
|
|
46
50
|
/** Tool/skill surfaces whose fold is intersection-with-baseline (capability ceiling), not assignment. */
|
|
47
|
-
const INTERSECTION_PATCH_KEYS = [
|
|
51
|
+
const INTERSECTION_PATCH_KEYS = [
|
|
52
|
+
"allowedToolIds",
|
|
53
|
+
"baselineToolIds",
|
|
54
|
+
"stableCoreToolIds",
|
|
55
|
+
"skillFilter",
|
|
56
|
+
];
|
|
48
57
|
const RUNTIME_PATCH_KEYS = [
|
|
49
58
|
"maxTurns",
|
|
50
59
|
"maxTotalTokens",
|
|
@@ -54,27 +63,29 @@ const RUNTIME_PATCH_KEYS = [
|
|
|
54
63
|
"knowledgeBudgetRatio",
|
|
55
64
|
"skillLeaseTurns",
|
|
56
65
|
"allowedToolIds",
|
|
66
|
+
"baselineToolIds",
|
|
57
67
|
"stableCoreToolIds",
|
|
58
68
|
"enablePlanTool",
|
|
59
69
|
"skillFilter",
|
|
60
70
|
...MEMORY_POLICY_PATCH_KEYS,
|
|
61
71
|
];
|
|
62
|
-
/** Bounds for the id-list surfaces (allowedToolIds / stableCoreToolIds / skillFilter). */
|
|
72
|
+
/** Bounds for the id-list surfaces (allowedToolIds / baselineToolIds / stableCoreToolIds / skillFilter). */
|
|
63
73
|
const MAX_TOOL_ID_CHARS = 128;
|
|
64
74
|
const MAX_TOOL_LIST_ENTRIES = 128;
|
|
65
75
|
/**
|
|
66
76
|
* Map an editable surface to its promotion tier. Maintained HERE beside the whitelist so a surface can
|
|
67
|
-
* never be whitelisted without also being assigned a tier (spec
|
|
77
|
+
* never be whitelisted without also being assigned a tier (the spec's same-place maintenance rule):
|
|
68
78
|
* - "auto" (Tier A): every `runtime.*` whitelist surface. Typed validation + the capability
|
|
69
|
-
* ceiling invariant + the
|
|
79
|
+
* ceiling invariant + the acceptance rule already guard them, so promotion is fully
|
|
70
80
|
* automatic — there is no free text and no injection surface.
|
|
71
81
|
* - "screened" (Tier B): `instructions.*` and `nudges`. Free text can smuggle instructions
|
|
72
82
|
* (persistent prompt-injection laundered through the evidence loop), so a screen runs
|
|
73
83
|
* before promotion.
|
|
74
|
-
* - "human" (Tier C): reserved for capability-WIDENING surfaces. None exist
|
|
75
|
-
* semantics make widening structurally inexpressible — but the enum value exists so a
|
|
76
|
-
* surface cannot be added without consciously assigning it a tier (and
|
|
77
|
-
* human gate). `surfaceTier` therefore never returns "human"
|
|
84
|
+
* - "human" (Tier C): reserved for capability-WIDENING surfaces. None currently exist — intersection
|
|
85
|
+
* semantics make widening structurally inexpressible — but the enum value exists so a new
|
|
86
|
+
* capability-widening surface cannot be added without consciously assigning it a tier (and
|
|
87
|
+
* building the human gate). `surfaceTier` therefore never returns "human" today — no
|
|
88
|
+
* capability-widening surface is expressible.
|
|
78
89
|
* An unknown surface / slot / runtime key THROWS (same discipline as applySurfaceEdit): a surface with
|
|
79
90
|
* no tier must never fall through to auto-promotion.
|
|
80
91
|
*/
|
|
@@ -164,23 +175,19 @@ function validateRuntimePatch(runtime) {
|
|
|
164
175
|
}
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
* `allowEmpty` is the load-bearing asymmetry. For the tool-id arrays it is FALSE: the runner reads an
|
|
170
|
-
* empty/absent `allowedToolIds` as "no gating — expose ALL registered tools", so an empty array would
|
|
171
|
-
* WIDEN exposure to everything if it reached the runner (and a zero-tool run is the v0.2.46 pathology).
|
|
172
|
-
* For `skillFilter` it is TRUE: the runner's no-gating sentinel is ONLY `undefined`, and an empty array
|
|
173
|
-
* legitimately means "no skills available" (a proposer may find skills are a distraction) — a narrowing.
|
|
174
|
-
*/
|
|
175
|
-
function validateIdList(key, value, allowEmpty) {
|
|
178
|
+
/** Validate an id-list surface: array of unique, non-empty strings (each ≤128 chars), ≤128 entries. */
|
|
179
|
+
function validateIdList(key, value, emptyPolicy) {
|
|
176
180
|
if (!Array.isArray(value))
|
|
177
181
|
throw new TypeError(`runtime.${key} must be a string[]`);
|
|
178
182
|
if (value.length > MAX_TOOL_LIST_ENTRIES) {
|
|
179
183
|
throw new RangeError(`runtime.${key} exceeds ${MAX_TOOL_LIST_ENTRIES} entries`);
|
|
180
184
|
}
|
|
181
|
-
if (
|
|
185
|
+
if (emptyPolicy === "reject:widens" && value.length === 0) {
|
|
182
186
|
throw new RangeError(`runtime.${key} must be a non-empty list — an empty array is read by the runner as "no gating" (expose all registered tools), which WIDENS exposure`);
|
|
183
187
|
}
|
|
188
|
+
if (emptyPolicy === "reject:drastic" && value.length === 0) {
|
|
189
|
+
throw new RangeError(`runtime.${key} must be a non-empty list — an empty baseline collapses the pre-activation surface to meta-tools only, which stays a human/host decision`);
|
|
190
|
+
}
|
|
184
191
|
const seen = new Set();
|
|
185
192
|
for (const entry of value) {
|
|
186
193
|
if (typeof entry !== "string" || entry.length === 0) {
|
|
@@ -218,10 +225,13 @@ function validateRuntimeValue(key, value) {
|
|
|
218
225
|
return;
|
|
219
226
|
case "allowedToolIds":
|
|
220
227
|
case "stableCoreToolIds":
|
|
221
|
-
validateIdList(key, value,
|
|
228
|
+
validateIdList(key, value, "reject:widens");
|
|
229
|
+
return;
|
|
230
|
+
case "baselineToolIds":
|
|
231
|
+
validateIdList(key, value, "reject:drastic");
|
|
222
232
|
return;
|
|
223
233
|
case "skillFilter":
|
|
224
|
-
validateIdList(key, value,
|
|
234
|
+
validateIdList(key, value, "allow");
|
|
225
235
|
return;
|
|
226
236
|
case "knowledgeBudgetRatio":
|
|
227
237
|
if (typeof value !== "number" || !(value > 0 && value <= 1)) {
|
|
@@ -334,7 +344,9 @@ export function applyManifest(manifest, base) {
|
|
|
334
344
|
}
|
|
335
345
|
/**
|
|
336
346
|
* Fold one intersection surface (capability ceiling): effective = manifest ∩ host-baseline, so a
|
|
337
|
-
* manifest can only NARROW. The empty-baseline meaning is surface-specific and load-bearing
|
|
347
|
+
* manifest can only NARROW. The empty-baseline meaning is surface-specific and load-bearing — it
|
|
348
|
+
* tracks whatever the RUNNER's own no-gating sentinel is for that option, not whether the option
|
|
349
|
+
* happens to hold tool ids:
|
|
338
350
|
*
|
|
339
351
|
* - allowedToolIds / stableCoreToolIds — the runner reads an empty OR absent baseline as
|
|
340
352
|
* "no gating = all registered tools" (the universe), so a non-array/empty baseline yields the
|
|
@@ -342,19 +354,22 @@ export function applyManifest(manifest, base) {
|
|
|
342
354
|
* empty intersection THROWS: a zero-tool run reprises the v0.2.46 pathology AND the runner would
|
|
343
355
|
* silently reinterpret the empty result as "no gating" (full exposure) — so we turn the candidate
|
|
344
356
|
* into a discardable error instead.
|
|
345
|
-
* - skillFilter — the runner's no-gating sentinel is ONLY `undefined`; an
|
|
346
|
-
* genuine, maximally-tight ceiling (no skills
|
|
347
|
-
*
|
|
357
|
+
* - skillFilter / baselineToolIds — the runner's no-gating sentinel is ONLY `undefined`; an
|
|
358
|
+
* empty-array baseline is a genuine, maximally-tight ceiling (no skills / the minimal meta-only
|
|
359
|
+
* tool surface). So ANY present array (even `[]`) is intersected, and an empty result is FINE —
|
|
360
|
+
* it reaches the runner as exactly that maximally-tight value, never as "no gating". This mirrors
|
|
361
|
+
* the validation asymmetry exactly. `baselineToolIds` sits on THIS side despite being a tool-id
|
|
362
|
+
* list: `[]` is its documented minimal surface (kernel `Some([])`), distinct from absent.
|
|
348
363
|
*/
|
|
349
364
|
function foldIntersection(key, manifestList, baseList) {
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
const constrained = Array.isArray(baseList) && (
|
|
365
|
+
// Is an EMPTY host baseline a maximally-tight ceiling (intersect it, empty result fine) or the
|
|
366
|
+
// universe (ignore it, empty result is a bug)?
|
|
367
|
+
const emptyBaselineIsTight = key === "skillFilter" || key === "baselineToolIds";
|
|
368
|
+
const constrained = Array.isArray(baseList) && (emptyBaselineIsTight || baseList.length > 0);
|
|
354
369
|
const effective = constrained
|
|
355
370
|
? manifestList.filter(id => baseList.includes(id)) // manifest order → deterministic
|
|
356
371
|
: manifestList;
|
|
357
|
-
if (!
|
|
372
|
+
if (!emptyBaselineIsTight && effective.length === 0) {
|
|
358
373
|
throw new RangeError(`applyManifest: runtime.${key} intersection is empty — manifest [${manifestList.join(", ")}] ∩ ` +
|
|
359
374
|
`host [${(baseList ?? []).join(", ")}] names no shared tool. A zero-tool run is rejected (it ` +
|
|
360
375
|
`reprises the v0.2.46 pathology and the runner would read empty as "no gating" = full exposure).`);
|
package/dist/harness/nudge.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-Harness
|
|
2
|
+
* Self-Harness nudge rules — declarative event→note rules (the runtime control-policy surface).
|
|
3
3
|
*
|
|
4
4
|
* A `NudgeRule` says "when this session event fires, push this note to the model". It generalizes the
|
|
5
5
|
* two hard-coded precedents (EntropyWatch.notify_model, the RepeatFuse STOP text) into data the
|
package/dist/harness/public.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { AttemptLoop, RuntimeAttemptBody, continueSession, freshWithFeedback, freshWithDigest, } from "./harness.js";
|
|
3
3
|
export { VerdictFnJudge, LlmEvalJudge, HybridJudge } from "./judge.js";
|
|
4
4
|
export { judge } from "../runtime/eval.js";
|
|
5
|
-
// Self-Harness
|
|
5
|
+
// Self-Harness editable surfaces: the harness face as data (manifest lineage + declarative event→note rules). The
|
|
6
6
|
// lab layer loads these through the compiled dist, so they live on this public barrel.
|
|
7
7
|
export { composeSystemPrompt, manifestDigest, applyManifest, applyPatch, validateManifest, surfaceTier, } from "./manifest.js";
|
|
8
8
|
export { NudgeEngine, validateNudgeRules } from "./nudge.js";
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// Mirrors the kernel's EXPOSURE_EXEMPT_META_TOOLS invariant: kernel-owned meta surfaces are
|
|
2
|
+
// never narrowed away by a tool allow-list. `read_result` is runner-resolved before reaching any
|
|
3
|
+
// plane today, but the lists must not drift.
|
|
4
|
+
const DEFAULT_META_TOOLS = new Set(["skill", "memory", "knowledge", "update_plan", "read_result"]);
|
|
2
5
|
/** Wraps an execution plane, allowing only manifest-permitted tool IDs (+ meta-tools). */
|
|
3
6
|
export class FilteredExecutionPlane {
|
|
4
7
|
inner;
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -144,12 +144,12 @@ export interface RuntimeOptions {
|
|
|
144
144
|
phase?: "initial" | "renewal";
|
|
145
145
|
}) => Promise<MemoryQuery[] | undefined> | MemoryQuery[] | undefined;
|
|
146
146
|
systemPrompt?: string;
|
|
147
|
-
/** Self-Harness
|
|
147
|
+
/** Self-Harness instruction surface: the four instruction slots (bootstrap/execution/verification/failureRecovery)
|
|
148
148
|
* composed onto `systemPrompt` in fixed order at option normalization. The kernel still sees ONE
|
|
149
149
|
* system prompt; this is the editable instruction surface the self-harness loop rewrites. Absent ⇒
|
|
150
150
|
* `systemPrompt` is used verbatim (zero behavior difference). */
|
|
151
151
|
instructions?: InstructionProfile;
|
|
152
|
-
/** Self-Harness
|
|
152
|
+
/** Self-Harness nudge surface: declarative event→note rules. On each matching session event a rendered note
|
|
153
153
|
* is pushed through the `injectNote` signal channel (same path as `onToolResult`'s `{note}`).
|
|
154
154
|
* ≤16 rules, validated at construction. Absent/empty ⇒ no engine, no append wrapping, zero
|
|
155
155
|
* behavior difference. */
|
|
@@ -306,13 +306,57 @@ export interface RuntimeOptions {
|
|
|
306
306
|
}) => Promise<MilestoneCheckResult> | MilestoneCheckResult;
|
|
307
307
|
/** Passed to kernel start_run for role/isolation metadata. */
|
|
308
308
|
runSpec?: AgentRunSpec;
|
|
309
|
-
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
309
|
+
/**
|
|
310
|
+
* The run's **exposure ceiling** — the outer bound on what this run may EVER advertise to the
|
|
311
|
+
* model. Not a static profile: it is an INTERSECTION applied on every turn (`exposed ⊆ ceiling`),
|
|
312
|
+
* so every narrowing mechanism operates *within* it and none can widen past it. Skills narrow
|
|
313
|
+
* inside the ceiling (`allowed_tools`), `baselineToolIds` selects which of the ceiling's tools are
|
|
314
|
+
* exposed before any skill activates, `stableCoreToolIds` pins tools against skill narrowing, and
|
|
315
|
+
* the self-harness manifest surface folds by intersection for exactly this reason.
|
|
316
|
+
*
|
|
317
|
+
* Exempt on the id axis: the kernel-owned meta-tools (`skill`, `memory`, `knowledge`,
|
|
318
|
+
* `update_plan`, `read_result`) stay exposed regardless of this list — a ceiling that hid `skill`
|
|
319
|
+
* would make progressive disclosure unreachable. The KIND axis
|
|
320
|
+
* (`runSpec.capabilityFilter.allowedKinds`) still applies to them.
|
|
321
|
+
*
|
|
322
|
+
* Byte-stable across the run, so it never busts the prompt-cache prefix. Lowers to the same
|
|
323
|
+
* `capability_filter` sub-agents use: augments `runSpec`'s filter when both are set, else
|
|
324
|
+
* synthesizes a minimal run spec. Omitted **or empty** ⇒ no ceiling (all registered tools) — the
|
|
325
|
+
* empty array is NOT a minimal surface here; use `baselineToolIds: []` for that.
|
|
326
|
+
*
|
|
327
|
+
* Enforcement: `toolDispatchGate` (default `"exposed"`) makes this a real boundary — a call to a
|
|
328
|
+
* tool outside the advertised set never executes.
|
|
329
|
+
*/
|
|
315
330
|
allowedToolIds?: string[];
|
|
331
|
+
/**
|
|
332
|
+
* The **pre-activation** exposure surface, selected from under the `allowedToolIds` ceiling.
|
|
333
|
+
* Makes the narrow→wide progressive-disclosure shape expressible: start the run advertising only
|
|
334
|
+
* these tools, and let a skill activation widen the surface by exactly its declared
|
|
335
|
+
* `allowed_tools` (still ∩ the ceiling). Per turn:
|
|
336
|
+
*
|
|
337
|
+
* `exposed = meta ∪ ((baseline ∪ stableCore ∪ ⋃ activeSkills.allowed_tools) ∩ ceiling)`
|
|
338
|
+
*
|
|
339
|
+
* An active skill that declares no `allowed_tools` contributes nothing — with a baseline set the
|
|
340
|
+
* surface stays narrow (strict; the legacy errs-open widening is deliberately not inherited).
|
|
341
|
+
*
|
|
342
|
+
* `undefined` ⇒ legacy behavior, byte-identical (ceiling + errs-open skill narrowing). `[]` is a
|
|
343
|
+
* legitimate, distinct value: the minimal surface (meta-tools + `stableCoreToolIds` only) — the
|
|
344
|
+
* `allowedToolIds` "empty means no gating" trap does NOT recur here. Entries outside the ceiling
|
|
345
|
+
* silently intersect away (no start_run error), the same fold every id-list surface uses.
|
|
346
|
+
*/
|
|
347
|
+
baselineToolIds?: string[];
|
|
348
|
+
/**
|
|
349
|
+
* Dispatch enforcement for the exposure surface. `"exposed"` (default) is fail-closed: a tool call
|
|
350
|
+
* the model was never advertised this turn never reaches the host — the kernel commits a
|
|
351
|
+
* model-visible `governance_denied` result instead ("Tool 'X' is not part of this run's toolset"),
|
|
352
|
+
* which feeds the repeat fuse like any other denial. Allowed siblings in the same batch still
|
|
353
|
+
* execute; `pace` and the meta-tool family always pass through.
|
|
354
|
+
*
|
|
355
|
+
* `"registered"` is the escape hatch restoring the pre-gate permissive behavior (any registered
|
|
356
|
+
* tool the model names executes, even if it was gated out of the tools schema). Set it only when a
|
|
357
|
+
* host deliberately relies on blind calls to unadvertised tools.
|
|
358
|
+
*/
|
|
359
|
+
toolDispatchGate?: "exposed" | "registered";
|
|
316
360
|
/** P0-C: optional per-turn metrics sink for tool-gating telemetry (see `TurnMetrics`). Pure
|
|
317
361
|
* observation; invoked once per LLM turn. Never throws into the run loop (errors are swallowed). */
|
|
318
362
|
onTurnMetrics?: (metrics: TurnMetrics) => void;
|
package/dist/runtime/runner.js
CHANGED
|
@@ -438,6 +438,11 @@ export class RuntimeRunner {
|
|
|
438
438
|
if (this.opts.criteriaGate !== undefined) {
|
|
439
439
|
config.criteria_gate = this.opts.criteriaGate;
|
|
440
440
|
}
|
|
441
|
+
// P1: fail-closed dispatch selector (absent ⇒ kernel default "exposed"). "registered" is the
|
|
442
|
+
// escape hatch back to permissive dispatch; the kernel rejects any other value.
|
|
443
|
+
if (this.opts.toolDispatchGate !== undefined) {
|
|
444
|
+
config.tool_dispatch_gate = this.opts.toolDispatchGate;
|
|
445
|
+
}
|
|
441
446
|
// K2: knowledge budget ratio (absent ⇒ kernel default 0.25; 0 disables).
|
|
442
447
|
if (this.opts.knowledgeBudgetRatio !== undefined) {
|
|
443
448
|
config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
|
|
@@ -1579,21 +1584,28 @@ export class RuntimeRunner {
|
|
|
1579
1584
|
kind: "start_run",
|
|
1580
1585
|
task: { goal, criteria },
|
|
1581
1586
|
};
|
|
1582
|
-
// P0-A: lower an explicit `runSpec
|
|
1583
|
-
//
|
|
1584
|
-
// a minimal top-level spec carrying just the
|
|
1585
|
-
// new ABI). Unset on
|
|
1587
|
+
// P0-A: lower an explicit `runSpec`, the `allowedToolIds` ceiling, and/or the `baselineToolIds`
|
|
1588
|
+
// pre-activation surface to the kernel run spec. Each augments an explicit spec, else
|
|
1589
|
+
// synthesizes a minimal top-level spec carrying just the exposure config (reuses the existing
|
|
1590
|
+
// run_spec wire — no new ABI). Unset on all ⇒ no run_spec ⇒ no gating (铁律: no config = old
|
|
1591
|
+
// behavior).
|
|
1586
1592
|
const allowedToolIds = this.opts.allowedToolIds;
|
|
1587
1593
|
const hasProfile = allowedToolIds !== undefined && allowedToolIds.length > 0;
|
|
1588
|
-
|
|
1594
|
+
// NOT the `length > 0` idiom above: `baselineToolIds: []` is the legitimate minimal surface
|
|
1595
|
+
// (meta + stable-core only), so mere presence triggers the lowering.
|
|
1596
|
+
const baselineToolIds = this.opts.baselineToolIds;
|
|
1597
|
+
const hasBaseline = baselineToolIds !== undefined;
|
|
1598
|
+
if (this.opts.runSpec || hasProfile || hasBaseline) {
|
|
1589
1599
|
const baseSpec = this.opts.runSpec ?? {
|
|
1590
1600
|
identity: { agentId: this.opts.agentId ?? "root", sessionId, isSubAgent: false },
|
|
1591
1601
|
role: "custom",
|
|
1592
1602
|
goal,
|
|
1593
1603
|
};
|
|
1594
|
-
|
|
1604
|
+
let spec = hasProfile
|
|
1595
1605
|
? { ...baseSpec, capabilityFilter: { ...baseSpec.capabilityFilter, allowedIds: allowedToolIds } }
|
|
1596
1606
|
: baseSpec;
|
|
1607
|
+
if (hasBaseline)
|
|
1608
|
+
spec = { ...spec, exposureBaseline: baselineToolIds };
|
|
1597
1609
|
startPayload.run_spec = agentRunSpecToKernel(spec);
|
|
1598
1610
|
}
|
|
1599
1611
|
// Reserve capacity before start_run. The kernel enforces only this vehicle's grant and reports
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -41,6 +41,15 @@ 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: a tool can be reachable after `skill(x)` without being advertised beforehand.
|
|
49
|
+
* Absent ⇒ legacy behavior (ceiling + errs-open skill narrowing). `[]` is meaningful and
|
|
50
|
+
* distinct from absent: the minimal surface (meta-tools + stable-core only). Entries outside
|
|
51
|
+
* the ceiling silently intersect away. Lowered from `RuntimeOptions.baselineToolIds`. */
|
|
52
|
+
exposureBaseline?: string[];
|
|
44
53
|
/** M1/G3: per-agent model preference (e.g. "opus"/"sonnet"/"haiku"); the host resolves it to a
|
|
45
54
|
* provider via `RuntimeOptions.providerFor`. Host-side routing only — not sent to the kernel. */
|
|
46
55
|
modelHint?: string;
|
package/dist/types/agent.js
CHANGED
|
@@ -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/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.50",
|
|
4
4
|
"description": "DeepStrike Node.js SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@anthropic-ai/sdk": "^0.99.0",
|
|
75
|
-
"@deepstrike/core": "0.2.
|
|
75
|
+
"@deepstrike/core": "0.2.50",
|
|
76
76
|
"@google/generative-ai": "^0.24.1",
|
|
77
77
|
"openai": "^5.23.2"
|
|
78
78
|
},
|