@sema-agent/core 7.16.0 → 7.17.1
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/CHANGELOG.md +209 -0
- package/dist/agents/builtin-agents.d.ts +3 -3
- package/dist/agents/cross-session-envelope.d.ts +0 -1
- package/dist/agents/cross-session-envelope.js +0 -1
- package/dist/agents/peer-notices.d.ts +0 -9
- package/dist/agents/peer-notices.js +0 -63
- package/dist/agents/subagent.js +7 -15
- package/dist/brain/request-params.d.ts +2 -2
- package/dist/config/defaults.d.ts +0 -7
- package/dist/config/defaults.js +0 -2
- package/dist/core/artifact-host.d.ts +218 -0
- package/dist/core/artifact-host.js +92 -0
- package/dist/core/ask-origin.d.ts +29 -3
- package/dist/core/ask-origin.js +17 -9
- package/dist/core/ask-question.d.ts +25 -0
- package/dist/core/ask-question.js +4 -0
- package/dist/core/ask-unresolvable-notice.d.ts +30 -21
- package/dist/core/ask-unresolvable-notice.js +16 -8
- package/dist/core/auto-compaction.d.ts +10 -10
- package/dist/core/auto-mode-arming.d.ts +2 -2
- package/dist/core/background-agent-store.d.ts +5 -5
- package/dist/core/background-shell.d.ts +1 -1
- package/dist/core/checkpoint-execution-record.d.ts +0 -3
- package/dist/core/checkpoint-execution-record.js +0 -4
- package/dist/core/checkpoint-store.d.ts +71 -99
- package/dist/core/checkpoint-store.js +9 -16
- package/dist/core/context-edit.d.ts +36 -104
- package/dist/core/context-edit.js +37 -60
- package/dist/core/effective-path-target.d.ts +68 -8
- package/dist/core/effective-path-target.js +42 -30
- package/dist/core/engine-notice.d.ts +46 -32
- package/dist/core/env-budget.d.ts +31 -0
- package/dist/core/env-budget.js +39 -0
- package/dist/core/exec-output-tail.d.ts +5 -5
- package/dist/core/file-history-store.js +3 -6
- package/dist/core/fs-write-gate-policy.d.ts +1 -1
- package/dist/core/fs-write-gate-policy.js +3 -3
- package/dist/core/gate-lanes.js +9 -15
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hands-band.d.ts +1 -1
- package/dist/core/hooks.d.ts +18 -17
- package/dist/core/lsp-session.d.ts +1 -1
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/lsp.js +5 -4
- package/dist/core/mcp.d.ts +5 -5
- package/dist/core/mcp.js +4 -48
- package/dist/core/memory-engine/engine.js +2 -1
- package/dist/core/memory-engine/layout.d.ts +18 -6
- package/dist/core/memory-engine/layout.js +40 -21
- package/dist/core/memory-recall.d.ts +0 -8
- package/dist/core/memory.d.ts +1 -1
- package/dist/core/oracle-isolation.js +2 -2
- package/dist/core/parent-spec-seat.d.ts +66 -0
- package/dist/core/parent-spec-seat.js +47 -0
- package/dist/core/permission-rule-model.d.ts +11 -6
- package/dist/core/permission-rule-model.js +53 -37
- package/dist/core/permission-rules.js +8 -3
- package/dist/core/physical-path.d.ts +37 -0
- package/dist/core/physical-path.js +30 -0
- package/dist/core/protocol-naming.d.ts +2 -2
- package/dist/core/remote-env.d.ts +1 -1
- package/dist/core/retired-keys.d.ts +19 -0
- package/dist/core/retired-keys.js +15 -0
- package/dist/core/runner/active-skill-scope.js +3 -3
- package/dist/core/runner/assemble-result.d.ts +15 -13
- package/dist/core/runner/assemble-result.js +5 -4
- package/dist/core/runner/compaction-call-options.d.ts +25 -89
- package/dist/core/runner/contracts.d.ts +11 -10
- package/dist/core/runner/gate-exit.js +15 -1
- package/dist/core/runner/halt-attribution.d.ts +35 -0
- package/dist/core/runner/halt-attribution.js +16 -0
- package/dist/core/runner/memory-consolidation.d.ts +0 -2
- package/dist/core/runner/memory-consolidation.js +0 -1
- package/dist/core/runner/prepare-artifact.d.ts +52 -0
- package/dist/core/runner/prepare-artifact.js +63 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
- package/dist/core/runner/prepare-config-doors.d.ts +3 -3
- package/dist/core/runner/prepare-config-doors.js +5 -2
- package/dist/core/runner/prepare-context-lane.d.ts +2 -2
- package/dist/core/runner/prepare-context-lane.js +2 -13
- package/dist/core/runner/prepare-file-history.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.js +3 -52
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
- package/dist/core/runner/prepare-inherited-gate.js +7 -8
- package/dist/core/runner/prepare-memory.d.ts +4 -6
- package/dist/core/runner/prepare-memory.js +4 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
- package/dist/core/runner/prepare-policy-chain.js +15 -30
- package/dist/core/runner/prepare-prompt-assembly.js +1 -1
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
- package/dist/core/runner/prepare-suspend-saga.js +0 -2
- package/dist/core/runner/prepare-task.js +12 -11
- package/dist/core/runner/prepare-wiring-manifest.js +9 -7
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-internals-and-config.js +4 -0
- package/dist/core/runner/retired-runner-deps.d.ts +3 -0
- package/dist/core/runner/retired-runner-deps.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +5 -3
- package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
- package/dist/core/runner/run-settle-and-teardown.js +3 -1
- package/dist/core/runner/session-file-state-replay.js +3 -3
- package/dist/core/runner/session-rule-policy.js +3 -4
- package/dist/core/runner/stream-halt-verbs.js +1 -1
- package/dist/core/runner/stream-steer-verb.js +1 -4
- package/dist/core/runner/workspace-path.js +3 -0
- package/dist/core/runner-deps.d.ts +30 -49
- package/dist/core/safe-notify.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/sensitive-path-policy.js +11 -12
- package/dist/core/session.d.ts +2 -2
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +4 -3
- package/dist/core/spec-contract.js +5 -4
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/strategy-store.d.ts +8 -11
- package/dist/core/task-event.d.ts +20 -17
- package/dist/core/task-limits.d.ts +1 -1
- package/dist/core/task-notification.d.ts +5 -5
- package/dist/core/task-registry-agent.d.ts +5 -5
- package/dist/core/task-registry-monitor.d.ts +2 -2
- package/dist/core/task-registry-shared.d.ts +32 -28
- package/dist/core/task-registry-shared.js +4 -13
- package/dist/core/task-registry.d.ts +8 -8
- package/dist/core/task-result.d.ts +37 -37
- package/dist/core/task-spec.d.ts +5 -5
- package/dist/core/task-stream.d.ts +16 -11
- package/dist/core/tool-catalog-entries.d.ts +1 -0
- package/dist/core/tool-catalog-entries.js +6 -1
- package/dist/core/tool-conformance.d.ts +0 -2
- package/dist/core/tool-conformance.js +1 -3
- package/dist/core/tool-errors.d.ts +5 -0
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +66 -22
- package/dist/core/tool-policy.js +31 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +11 -11
- package/dist/core/tool-spec.d.ts +34 -38
- package/dist/core/tools.d.ts +2 -2
- package/dist/core/trace.d.ts +12 -15
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +4 -3
- package/dist/core/workflow-journal-store.d.ts +11 -11
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
- package/dist/engine/execution-env/node-execution-env.js +8 -5
- package/dist/engine/harness/agent-harness.d.ts +47 -4
- package/dist/engine/harness/agent-harness.js +52 -9
- package/dist/engine/loop/agent-loop.js +15 -8
- package/dist/engine/loop/types.d.ts +5 -0
- package/dist/index.d.ts +20 -14
- package/dist/index.js +16 -10
- package/dist/internal/harness.d.ts +1 -0
- package/dist/orchestration/run-spec.d.ts +3 -0
- package/dist/orchestration/run-spec.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +12 -6
- package/dist/orchestration/run-workflow-tool.js +23 -5
- package/dist/orchestration/workflow-primitives.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +1 -4
- package/dist/orchestration/workflow-script-store.js +9 -25
- package/dist/orchestration/workflow-types.d.ts +34 -4
- package/dist/orchestration/workflow-types.js +2 -0
- package/dist/orchestration/workflow.d.ts +39 -7
- package/dist/orchestration/workflow.js +237 -113
- package/dist/prompt-assembly/assemble.d.ts +2 -2
- package/dist/prompt-assembly/assemble.js +9 -9
- package/dist/prompt-assembly/composer.d.ts +6 -7
- package/dist/prompt-assembly/composer.js +10 -10
- package/dist/prompt-assembly/epoch.js +1 -1
- package/dist/prompt-assembly/event-registry.d.ts +0 -21
- package/dist/prompt-assembly/packs/sema-default.js +30 -30
- package/dist/prompt-assembly/types.d.ts +14 -14
- package/dist/prompts/coordinator.d.ts +0 -13
- package/dist/prompts/coordinator.js +0 -3
- package/dist/stores/cc/task-list-store.js +2 -10
- package/dist/stores/file/checkpoint-store.js +1 -5
- package/dist/stores/file/file-history-store.js +2 -2
- package/dist/stores/file/fs-atomic.d.ts +15 -18
- package/dist/stores/file/fs-atomic.js +4 -14
- package/dist/stores/file/mailbox-store.d.ts +7 -11
- package/dist/stores/file/mailbox-store.js +4 -11
- package/dist/stores/file/permission-rule-store.d.ts +4 -35
- package/dist/stores/file/permission-rule-store.js +4 -148
- package/dist/tools/artifact/artifact-text.d.ts +62 -0
- package/dist/tools/artifact/artifact-text.js +68 -0
- package/dist/tools/artifact/artifact-tool.d.ts +52 -0
- package/dist/tools/artifact/artifact-tool.js +351 -0
- package/dist/tools/artifact/local-stub.d.ts +26 -0
- package/dist/tools/artifact/local-stub.js +297 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +90 -8
- package/dist/tools/fs/bash-readonly-classifier.js +294 -70
- package/dist/tools/fs/fs-bash.d.ts +14 -10
- package/dist/tools/fs/fs-bash.js +50 -33
- package/dist/tools/fs/fs-search-tools.js +5 -2
- package/dist/tools/fs/fs-shared.d.ts +2 -0
- package/dist/tools/fs/fs-shared.js +3 -11
- package/dist/tools/fs/index.d.ts +4 -0
- package/dist/tools/fs/index.js +4 -1
- package/dist/tools/fs/safety.d.ts +117 -1
- package/dist/tools/fs/safety.js +131 -31
- package/dist/tools/fs/search.js +8 -6
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +119 -39
|
@@ -4,6 +4,7 @@ import { compileReadDeny } from "../tools/fs/read-deny.js";
|
|
|
4
4
|
import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
5
5
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
6
6
|
import { pathTargetOf } from "./tool-registry.js";
|
|
7
|
+
import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
|
|
7
8
|
import { readShellCommand } from "./shell-lexer.js";
|
|
8
9
|
export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
|
|
9
10
|
const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
|
|
@@ -364,12 +365,18 @@ function baseValueOf(base, bases) {
|
|
|
364
365
|
}
|
|
365
366
|
}
|
|
366
367
|
function resolvePathPattern(pattern, bases, behavior) {
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
const family = pathFamilyOf(bases);
|
|
369
|
+
const tree = bases.root ?? bases.cwd;
|
|
370
|
+
if (pattern.startsWith("//")) {
|
|
371
|
+
const abs = pathIdentityOf("/" + pattern.slice(2), family, tree);
|
|
372
|
+
return abs.ok ? { resolved: abs.identity } : { missingBase: "root" };
|
|
373
|
+
}
|
|
369
374
|
const needed = ruleBasesNeeded({ match: "path", command: pattern })[0];
|
|
370
|
-
const
|
|
371
|
-
|
|
375
|
+
const baseValue = baseValueOf(needed, bases);
|
|
376
|
+
const base = baseValue !== undefined && isAbsoluteForFamily(family, baseValue) ? pathIdentityOf(baseValue, family, tree) : undefined;
|
|
377
|
+
if (base === undefined || !base.ok)
|
|
372
378
|
return { missingBase: needed };
|
|
379
|
+
const normal = base.identity;
|
|
373
380
|
const rest = pattern.startsWith("~/") ? pattern.slice(2) : pattern.startsWith("/") ? pattern.slice(1) : pattern.startsWith("./") ? pattern.slice(2) : pattern;
|
|
374
381
|
const peeled = rest.endsWith("/**") ? rest.slice(0, -3) : rest;
|
|
375
382
|
const bare = needed === "cwd" && peeled !== "" && !peeled.includes("/");
|
|
@@ -377,8 +384,8 @@ function resolvePathPattern(pattern, bases, behavior) {
|
|
|
377
384
|
const body = anyDepth ? "**/" + peeled : rest;
|
|
378
385
|
return { resolved: (normal === "/" ? "" : normal) + "/" + body };
|
|
379
386
|
}
|
|
380
|
-
export function isUsablePathBase(base) {
|
|
381
|
-
return base !== undefined &&
|
|
387
|
+
export function isUsablePathBase(base, family) {
|
|
388
|
+
return base !== undefined && isAbsoluteForFamily(family, base) && pathIdentityOf(base, family, base).ok;
|
|
382
389
|
}
|
|
383
390
|
function segmentGlobMatches(glob, segment) {
|
|
384
391
|
if (!glob.includes("*"))
|
|
@@ -422,15 +429,18 @@ function globSegmentsReach(globs, target) {
|
|
|
422
429
|
}
|
|
423
430
|
export function pathRuleReachOf(rule, target, bases) {
|
|
424
431
|
if (rule.match === "subpath")
|
|
425
|
-
return directoryRuleAdmits(rule, target) ? REACHED : NOT_REACHED;
|
|
432
|
+
return directoryRuleAdmits(rule, target, bases) ? REACHED : NOT_REACHED;
|
|
426
433
|
if (rule.match !== "path")
|
|
427
434
|
return NOT_REACHED;
|
|
428
|
-
|
|
435
|
+
const family = pathFamilyOf(bases);
|
|
436
|
+
if (!isPathIdentity(target, family))
|
|
429
437
|
return NOT_REACHED;
|
|
430
438
|
const r = resolvePathPattern(rule.command, bases, rule.behavior);
|
|
431
439
|
if ("missingBase" in r) {
|
|
432
|
-
return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base to resolve it against` };
|
|
440
|
+
return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base of this tree's family to resolve it against` };
|
|
433
441
|
}
|
|
442
|
+
if (isUncIdentity(r.resolved) !== isUncIdentity(target))
|
|
443
|
+
return NOT_REACHED;
|
|
434
444
|
const globs = r.resolved.split("/").filter((sg) => sg !== "");
|
|
435
445
|
const segments = target.split("/").filter((sg) => sg !== "");
|
|
436
446
|
return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
|
|
@@ -587,21 +597,9 @@ export function pathWithinRoot(path, root) {
|
|
|
587
597
|
const base = root.endsWith("/") ? root : root + "/";
|
|
588
598
|
return path.startsWith(base);
|
|
589
599
|
}
|
|
590
|
-
export function lexicalNormalAbsolutePathOf(path) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const out = [];
|
|
594
|
-
for (const seg of path.split("/")) {
|
|
595
|
-
if (seg === "" || seg === ".")
|
|
596
|
-
continue;
|
|
597
|
-
if (seg === "..") {
|
|
598
|
-
if (out.length > 0)
|
|
599
|
-
out.pop();
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
out.push(seg);
|
|
603
|
-
}
|
|
604
|
-
return out.length === 0 ? "/" : "/" + out.join("/");
|
|
600
|
+
export function lexicalNormalAbsolutePathOf(path, family, anchor) {
|
|
601
|
+
const r = pathIdentityOf(path, family, anchor);
|
|
602
|
+
return r.ok ? r.identity : undefined;
|
|
605
603
|
}
|
|
606
604
|
function isLexicalNormalAbsoluteDir(path) {
|
|
607
605
|
if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//"))
|
|
@@ -609,14 +607,22 @@ function isLexicalNormalAbsoluteDir(path) {
|
|
|
609
607
|
const segments = path.split("/").slice(1);
|
|
610
608
|
return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
|
|
611
609
|
}
|
|
612
|
-
export function directoryRuleAdmits(rule, path) {
|
|
610
|
+
export function directoryRuleAdmits(rule, path, bases) {
|
|
613
611
|
if (rule.match !== "subpath")
|
|
614
612
|
return false;
|
|
615
|
-
|
|
613
|
+
const family = pathFamilyOf(bases);
|
|
614
|
+
if (carriesDotSegment(rule.command, family))
|
|
615
|
+
return false;
|
|
616
|
+
const dir = pathIdentityOf(rule.command, family, bases.root ?? bases.cwd);
|
|
617
|
+
if (!dir.ok || !isIdentityDir(dir.identity, family))
|
|
616
618
|
return false;
|
|
617
|
-
if (!
|
|
619
|
+
if (!isPathIdentity(path, family) || !isIdentityDir(path, family))
|
|
618
620
|
return false;
|
|
619
|
-
return pathWithinRoot(path,
|
|
621
|
+
return pathWithinRoot(path, dir.identity);
|
|
622
|
+
}
|
|
623
|
+
export function directoryIdentityOf(spelling, bases) {
|
|
624
|
+
const r = pathIdentityOf(spelling, pathFamilyOf(bases), bases.root ?? bases.cwd);
|
|
625
|
+
return r.ok ? r.identity : undefined;
|
|
620
626
|
}
|
|
621
627
|
const CD_TARGET_REJECT_CHARS = /[*?[\]{}$`\\'"<>\n\r]/;
|
|
622
628
|
function cdSegmentDirectoryOf(segment, opts) {
|
|
@@ -711,7 +717,8 @@ function resolveCdSegmentDirectories(segments, cwd) {
|
|
|
711
717
|
const dir = cdSegmentDirectoryOf(segment, { cwd, priorCd });
|
|
712
718
|
if (!priorCd && movesShellCursor(segment))
|
|
713
719
|
priorCd = true;
|
|
714
|
-
|
|
720
|
+
const bases = basesOfCwd(cwd);
|
|
721
|
+
return dir === undefined || isShellRootedSpellingUnmapped(pathFamilyOf(bases), dir) ? undefined : directoryIdentityOf(dir, bases);
|
|
715
722
|
});
|
|
716
723
|
}
|
|
717
724
|
export function scopeCoversCwd(scope, cwd, sessionId) {
|
|
@@ -767,11 +774,14 @@ function firstEligibleDirectoryRule(rules, directory, call) {
|
|
|
767
774
|
for (const rule of rules) {
|
|
768
775
|
if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }, "allow"))
|
|
769
776
|
continue;
|
|
770
|
-
if (directoryRuleAdmits(rule, directory))
|
|
777
|
+
if (directoryRuleAdmits(rule, directory, basesOfCwd(call.execCwd ?? call.cwd)))
|
|
771
778
|
return rule;
|
|
772
779
|
}
|
|
773
780
|
return undefined;
|
|
774
781
|
}
|
|
782
|
+
function basesOfCwd(cwd) {
|
|
783
|
+
return cwd === undefined ? {} : { root: cwd };
|
|
784
|
+
}
|
|
775
785
|
export function adjudicatePersistedRules(rules, call) {
|
|
776
786
|
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
777
787
|
if (behavior === "allow") {
|
|
@@ -798,23 +808,29 @@ export function adjudicatePersistedRules(rules, call) {
|
|
|
798
808
|
export function adjudicatePersistedPathRules(rules, call, targets, bases) {
|
|
799
809
|
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
800
810
|
if (behavior === "allow") {
|
|
801
|
-
if (targets.allow
|
|
811
|
+
if (targets.allow?.kind !== "path" || call.tool !== READ_RULE_TOOL)
|
|
802
812
|
continue;
|
|
803
813
|
for (const rule of rules) {
|
|
804
814
|
if (!eligiblePersisted(rule, call, behavior))
|
|
805
815
|
continue;
|
|
806
|
-
if (directoryRuleAdmits(rule, targets.allow))
|
|
816
|
+
if (directoryRuleAdmits(rule, targets.allow.identity, bases))
|
|
807
817
|
return { behavior, rules: [rule] };
|
|
808
818
|
}
|
|
809
819
|
continue;
|
|
810
820
|
}
|
|
811
|
-
|
|
821
|
+
const tighten = targets.tighten;
|
|
822
|
+
if (tighten === undefined || tighten.kind === "refused")
|
|
812
823
|
continue;
|
|
813
824
|
let unreadable;
|
|
814
825
|
for (const rule of rules) {
|
|
815
826
|
if (!eligiblePersisted(rule, call, behavior))
|
|
816
827
|
continue;
|
|
817
|
-
|
|
828
|
+
if (tighten.kind === "unreadable") {
|
|
829
|
+
if (rule.match === "path" || rule.match === "subpath")
|
|
830
|
+
unreadable ??= { behavior: "ask", rules: [rule], unreadable: tighten.reason };
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const outcome = pathRuleReachOf(rule, tighten.identity, bases);
|
|
818
834
|
if (outcome.reach === "reached")
|
|
819
835
|
return { behavior, rules: [rule] };
|
|
820
836
|
if (outcome.reach === "unreadable")
|
|
@@ -843,7 +859,7 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
843
859
|
proposed.some((p) => p.rule.tool === "Read"
|
|
844
860
|
? dir !== undefined &&
|
|
845
861
|
eligibleContext({ tool: p.rule.tool, scope: p.scope }, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }) &&
|
|
846
|
-
directoryRuleAdmits(p.rule, dir)
|
|
862
|
+
directoryRuleAdmits(p.rule, dir, basesOfCwd(call.execCwd ?? call.cwd))
|
|
847
863
|
: eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
|
|
848
864
|
};
|
|
849
865
|
});
|
|
@@ -889,7 +905,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
889
905
|
const parsed = parseRuleText(formatRuleText(directory, "subpath", READ_RULE_TOOL), "allow");
|
|
890
906
|
if (!("rule" in parsed) || parsed.rule.match !== "subpath")
|
|
891
907
|
return undefined;
|
|
892
|
-
if (!directoryRuleAdmits(parsed.rule, directory))
|
|
908
|
+
if (!directoryRuleAdmits(parsed.rule, directory, basesOfCwd(ctx?.execCwd ?? ctx?.cwd)))
|
|
893
909
|
return undefined;
|
|
894
910
|
return { kind: "directoryRead", rule: parsed.rule.rule, directory: parsed.rule.command, segment };
|
|
895
911
|
};
|
|
@@ -963,7 +979,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
963
979
|
if (parsedBatch.some((p) => p.tool !== "Read" && ruleAdmitsCommand(p, segment)))
|
|
964
980
|
continue;
|
|
965
981
|
const dir = cdDirs[i];
|
|
966
|
-
if (dir !== undefined && parsedBatch.some((p) => directoryRuleAdmits(p, dir)))
|
|
982
|
+
if (dir !== undefined && parsedBatch.some((p) => directoryRuleAdmits(p, dir, basesOfCwd(ctx?.execCwd ?? ctx?.cwd))))
|
|
967
983
|
continue;
|
|
968
984
|
const reason = carriesShellRedirection(segment) ? "redirection" : mintSegmentRule(segment) === undefined ? "no_rule_form" : "cap_overflow";
|
|
969
985
|
uncoveredDetail.push({ segment, reason });
|
|
@@ -2,6 +2,7 @@ import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
|
2
2
|
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
3
|
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
|
|
4
4
|
import { PATH_RULE_BASE_LABEL, isUsablePathBase, parseRuleText, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
|
|
5
|
+
import { pathFamilyOf } from "../tools/fs/safety.js";
|
|
5
6
|
import { effectivePathTargetOf } from "./effective-path-target.js";
|
|
6
7
|
import { protectivePathTargetOf } from "./tool-registry.js";
|
|
7
8
|
export { parsePermissionRule };
|
|
@@ -188,7 +189,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
188
189
|
}
|
|
189
190
|
for (const base of ruleBasesNeeded(parsedPath.rule)) {
|
|
190
191
|
const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
|
|
191
|
-
if (!isUsablePathBase(value)) {
|
|
192
|
+
if (!isUsablePathBase(value, pathFamilyOf(pathBases))) {
|
|
192
193
|
bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
@@ -321,9 +322,13 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
321
322
|
let unreadable;
|
|
322
323
|
for (const r of lane) {
|
|
323
324
|
if (r.parsed.match === "subpath" || r.parsed.match === "path") {
|
|
324
|
-
if (target === undefined)
|
|
325
|
+
if (target === undefined || target.kind === "refused")
|
|
325
326
|
continue;
|
|
326
|
-
|
|
327
|
+
if (target.kind === "unreadable") {
|
|
328
|
+
unreadable ??= { rule: r, unreadable: target.reason };
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const outcome = pathRuleReachOf(r.parsed, target.identity, pathBases);
|
|
327
332
|
if (outcome.reach === "reached")
|
|
328
333
|
return { rule: r };
|
|
329
334
|
if (outcome.reach === "unreadable")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE physical path of a path — one spelling for the whole tree.
|
|
3
|
+
*
|
|
4
|
+
* A physical path is what `realpath(3)` answers: every symlink resolved and, on a case-insensitive
|
|
5
|
+
* volume, the directory entry's ON-DISK spelling. That last half is the reason this module exists.
|
|
6
|
+
* Node has two realpaths and they disagree exactly there (measured on macOS): `realpathSync.native`
|
|
7
|
+
* and `fsPromises.realpath` are the libuv call and return the disk case, while the JS `fs.realpathSync`
|
|
8
|
+
* resolves the links but PRESERVES the caller's casing. A tree that used both minted two spellings for
|
|
9
|
+
* one directory, so a key minted on one face could not be found on the other — a memory write root
|
|
10
|
+
* spelled `…/Proj` by configuration was keyed `…/Proj` by the engine's gates and resolved to `…/proj`
|
|
11
|
+
* by the execution env's fence, and the advertised-writable-directory seat had to refuse the hole its
|
|
12
|
+
* own instruction taught.
|
|
13
|
+
*
|
|
14
|
+
* Case FOLDING is not part of a path's identity and is not done here: folding a filesystem PATH names a
|
|
15
|
+
* different directory on a case-sensitive volume. A store whose in-memory KEY must fold (see
|
|
16
|
+
* `canonicalStoreKey`) folds this answer at its own call site.
|
|
17
|
+
*/
|
|
18
|
+
export declare function physicalPathOf(p: string): string;
|
|
19
|
+
/** The spelling the memory layout keyed by BEFORE this mint — links resolved, the caller's casing kept (the JS
|
|
20
|
+
* `fs.realpathSync`, which differs from the native call exactly there). Migration probes only: never an identity. */
|
|
21
|
+
export declare function priorCallerCasedPathOf(p: string): string;
|
|
22
|
+
/** The async door onto the same rule (`fsPromises.realpath` is the libuv call `realpathSync.native`
|
|
23
|
+
* makes). The `ExecutionEnv` faces take this one: a sync realpath against a hung network mount blocks
|
|
24
|
+
* the event loop for the mount timeout, and every pending abort with it. */
|
|
25
|
+
export declare function physicalPathOfAsync(p: string): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* {@link physicalPathOf} for a path that need not exist yet: the deepest ancestor that resolves,
|
|
28
|
+
* canonicalized, with the missing tail rejoined — so a not-yet-created target is still judged against
|
|
29
|
+
* roots that are physical paths, and a symlinked PARENT still cannot carry a write past them (the
|
|
30
|
+
* parent is exactly what gets resolved before the tail is rejoined).
|
|
31
|
+
*
|
|
32
|
+
* Total, by construction rather than by swallowing: a path with no resolvable ancestor at all has no
|
|
33
|
+
* physical form to answer with, and the absolute lexical form is that answer. Callers that must
|
|
34
|
+
* distinguish "unresolvable" from "does not exist yet" call {@link physicalPathOf} and handle the throw
|
|
35
|
+
* beside their own contract.
|
|
36
|
+
*/
|
|
37
|
+
export declare function physicalPathOfExisting(p: string): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
export function physicalPathOf(p) {
|
|
5
|
+
return realpathSync.native(p);
|
|
6
|
+
}
|
|
7
|
+
export function priorCallerCasedPathOf(p) {
|
|
8
|
+
return realpathSync(p);
|
|
9
|
+
}
|
|
10
|
+
export async function physicalPathOfAsync(p) {
|
|
11
|
+
return await realpath(p);
|
|
12
|
+
}
|
|
13
|
+
export function physicalPathOfExisting(p) {
|
|
14
|
+
const abs = resolve(p);
|
|
15
|
+
const missingTail = [];
|
|
16
|
+
let cur = abs;
|
|
17
|
+
for (;;) {
|
|
18
|
+
try {
|
|
19
|
+
const real = physicalPathOf(cur);
|
|
20
|
+
return missingTail.length === 0 ? real : join(real, ...[...missingTail].reverse());
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
const parent = dirname(cur);
|
|
24
|
+
if (parent === cur)
|
|
25
|
+
return abs;
|
|
26
|
+
missingTail.push(basename(cur));
|
|
27
|
+
cur = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -18,13 +18,13 @@ export declare const MINTED_TOOL_SEGMENT_MIN_CHARS = 16;
|
|
|
18
18
|
* live domain key here (the refresh splice). That case is decided by
|
|
19
19
|
* {@link findNamespacePrefixCollision} and refused at the mount, loudly; the mint stays total.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
21
|
+
* The pattern this enforces is `{1,64}`, and only the charset half was
|
|
22
22
|
* enforced. Both other halves matter for the same reason the charset does — an over-long or empty segment
|
|
23
23
|
* produces a name the provider rejects, which 400s the WHOLE model request, the exact failure the intake
|
|
24
24
|
* gate exists to contain ("a single misconfigured server never bricks the whole task").
|
|
25
25
|
*/
|
|
26
26
|
export declare function normalizeNameSegment(name: string): string;
|
|
27
|
-
/**
|
|
27
|
+
/** Force a normalized segment into `{1,max}`, keeping long names distinct via a stable digest. */
|
|
28
28
|
export declare function clampNameSegment(seg: string, max?: number): string;
|
|
29
29
|
/**
|
|
30
30
|
* The registered-name prefix for `peer` in `ns` — i.e. the set `name.startsWith(prefix)` that a consumer
|
|
@@ -508,7 +508,7 @@ export declare function missingRestoreSurface(env: ExecutionEnv): readonly ("res
|
|
|
508
508
|
* A composite type guard (structural AND capability) so a `suspendVM` call site narrowed by THIS predicate
|
|
509
509
|
* gets the {@link RemoteExecutionEnv} type without a cast — `true` always implies the structural check too.
|
|
510
510
|
*
|
|
511
|
-
*
|
|
511
|
+
* It also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
|
|
512
512
|
* if something can undo it; an adapter that offers `suspendVM` but no `resumeVM` used to pass this guard,
|
|
513
513
|
* take a real snapshot, commit a real checkpoint — and then blow up on the resume leg with an untyped
|
|
514
514
|
* TypeError. "Suspendable" now means the whole round trip, so a half-adapter is rejected at the suspend site
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE presence rule for a RETIRED key on a caller-supplied bag (a deps object, an options object): the key is
|
|
3
|
+
* PRESENT iff ordinary property lookup answers a defined value — prototype and non-enumerable members included,
|
|
4
|
+
* exactly what the former direct read of the seat honored; an explicit `undefined` reads as absence, as every
|
|
5
|
+
* optional seat reads. Every retired-key door reads through here so no door grows its own idea of "present";
|
|
6
|
+
* each composes its own refusal from the answer. A table row is `{ key: replacement }` — the sentence that says
|
|
7
|
+
* what replaced the seat, quoted in the refusal.
|
|
8
|
+
*/
|
|
9
|
+
/** The first retired key present on `bag`, in table order, or undefined when none is. */
|
|
10
|
+
export declare function retiredKeyOf(bag: object, table: Readonly<Record<string, string>>): {
|
|
11
|
+
key: string;
|
|
12
|
+
replacement: string;
|
|
13
|
+
} | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The deps-bag door: a retired seat still wired by a deployment is refused BY NAME (`config.deps_retired_key`)
|
|
16
|
+
* instead of vanishing silently — a seat that is silently dropped reads to the deployment as wired. `owner` is
|
|
17
|
+
* the bag's type name for the sentence (`RunnerDeps`, `RunWorkflowToolDeps`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertNoRetiredKeys(bag: object, table: Readonly<Record<string, string>>, owner: string): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function retiredKeyOf(bag, table) {
|
|
2
|
+
for (const [key, replacement] of Object.entries(table)) {
|
|
3
|
+
if (Reflect.get(bag, key) !== undefined)
|
|
4
|
+
return { key, replacement };
|
|
5
|
+
}
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
export function assertNoRetiredKeys(bag, table, owner) {
|
|
9
|
+
const hit = retiredKeyOf(bag, table);
|
|
10
|
+
if (hit === undefined)
|
|
11
|
+
return;
|
|
12
|
+
const e = new Error(`${owner}.${hit.key} is retired and is not read by this engine — ${hit.replacement}. Refused rather than ignored: a seat that is silently dropped reads to the deployment as wired.`);
|
|
13
|
+
e.code = "config.deps_retired_key";
|
|
14
|
+
throw e;
|
|
15
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalizeTarget } from "../../tools/fs/safety.js";
|
|
1
|
+
import { canonicalizeTarget, pathFamilyOf } from "../../tools/fs/safety.js";
|
|
2
2
|
import { pathTargetValue } from "../tool-registry.js";
|
|
3
3
|
import { pathTargetBaseOf } from "../effective-path-target.js";
|
|
4
4
|
import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
|
|
@@ -100,7 +100,7 @@ export function createActiveSkillScopePolicy(opts) {
|
|
|
100
100
|
decisionReason: "safety",
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
const canon = await canonicalizeTarget(env, path, signal, pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
|
|
103
|
+
const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootPath, cwd: req.cwd ?? env.cwd }), pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
|
|
104
104
|
if (!canon.ok) {
|
|
105
105
|
return {
|
|
106
106
|
action: "deny",
|
|
@@ -111,7 +111,7 @@ export function createActiveSkillScopePolicy(opts) {
|
|
|
111
111
|
for (const m of constraining) {
|
|
112
112
|
const roots = [];
|
|
113
113
|
for (const raw of m.allowPaths ?? []) {
|
|
114
|
-
const r = await canonicalizeTarget(env, raw, signal, rootPath);
|
|
114
|
+
const r = await canonicalizeTarget(env, raw, signal, pathFamilyOf({ root: rootPath ?? env.cwd }), rootPath);
|
|
115
115
|
if (r.ok)
|
|
116
116
|
roots.push(r.key);
|
|
117
117
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from "../../internal/llm.js";
|
|
2
2
|
import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
|
|
3
|
+
import type { HaltConsumption } from "../../internal/harness.js";
|
|
3
4
|
/** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
|
|
4
5
|
export interface Stats {
|
|
5
6
|
turns: number;
|
|
@@ -134,15 +135,14 @@ export interface ResultFlags {
|
|
|
134
135
|
* path the terminal is `completed`, and this is what tells that completion apart from a natural
|
|
135
136
|
* one — the model did not finish; the person stopped it and the run awaits their direction). */
|
|
136
137
|
haltedOnUserRejection?: boolean;
|
|
137
|
-
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
userHalted?: boolean;
|
|
138
|
+
/** the harness's STATEMENT of what the run's halt did (see `HaltConsumption`):
|
|
139
|
+
* the loop's report that the halt's cut settled the final turn, and/or the boundary consult that
|
|
140
|
+
* stopped the loop for the halt's request. This is the ONLY input to `TaskResult.haltedByUser`
|
|
141
|
+
* (`haltConsumed`, halt-attribution.ts) and to slot 9.7 — the assembly never infers causation from the
|
|
142
|
+
* final's shape or from the verb's acceptance. A halt that was accepted but consumed nothing (the run
|
|
143
|
+
* ended for its own reason first) leaves this empty: the original terminal stands and the seat is
|
|
144
|
+
* NOT signed (the settle lane mints `task.halt_unconsumed` for it). */
|
|
145
|
+
haltConsumption?: HaltConsumption;
|
|
146
146
|
/** Call ids of answered-but-never-collected questions, echoed on
|
|
147
147
|
* `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
|
|
148
148
|
* optional `onError` alert is NOT the disclosure — this mandatory result face is. */
|
|
@@ -298,13 +298,15 @@ export declare function terminalCauseOf(final: AssistantMessage | undefined, sta
|
|
|
298
298
|
* 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
|
|
299
299
|
* (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
|
|
300
300
|
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
|
|
301
|
-
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal
|
|
301
|
+
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`: the
|
|
302
|
+
* loop reported the halt's cut settled the final turn — never shape inference),
|
|
302
303
|
* and EXCEPT when the answer had already settled (`answerSettled`: a clean `"stop"` final, not
|
|
303
304
|
* partial) — a stop landing on the run's tail cuts no work, so the run falls through to 12
|
|
304
305
|
* (`completed`, the answer as `result`, no `limits.*` code for the axis; three axes, one law)
|
|
305
|
-
* 9.7 `
|
|
306
|
-
* bare user halt — the verb working, not a defect; below the
|
|
307
|
-
* abort/limit that also fired owns the terminal and the halt rides as
|
|
306
|
+
* 9.7 halt CONSUMED (`haltConsumed(flags.haltConsumption)`) + (no `final` | the halt's own aborted
|
|
307
|
+
* `final`) → status `"completed"` (a bare user halt — the verb working, not a defect; below the
|
|
308
|
+
* aborts on purpose, a real abort/limit that also fired owns the terminal and the halt rides as
|
|
309
|
+
* the pass-through seat; an accepted-but-unconsumed halt never reaches this slot)
|
|
308
310
|
* 10. no `final` → status `"failed"`
|
|
309
311
|
* 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
|
|
310
312
|
* 12. else → `"completed"`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
2
2
|
import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
|
|
3
3
|
import { terminalProjection } from "./terminal-projection.js";
|
|
4
|
+
import { haltConsumed } from "./halt-attribution.js";
|
|
4
5
|
const SALVAGE_ELIGIBLE_TERMINALS = new Set([
|
|
5
6
|
"output.degenerate",
|
|
6
7
|
"limits.max_tokens_exceeded",
|
|
@@ -47,7 +48,7 @@ export function answerSettled(final, continuationCut = false) {
|
|
|
47
48
|
export function terminalCauseOf(final, stats, flags) {
|
|
48
49
|
let terminal;
|
|
49
50
|
let apiFailure;
|
|
50
|
-
const haltOwnsAbortedFinal = flags.
|
|
51
|
+
const haltOwnsAbortedFinal = flags.haltConsumption?.turnCut === "halt" &&
|
|
51
52
|
final?.stopReason === "aborted" &&
|
|
52
53
|
!flags.abortedLive &&
|
|
53
54
|
flags.abortedForTimeout !== true &&
|
|
@@ -103,7 +104,7 @@ export function terminalCauseOf(final, stats, flags) {
|
|
|
103
104
|
const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
|
|
104
105
|
terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") };
|
|
105
106
|
}
|
|
106
|
-
else if (flags.
|
|
107
|
+
else if (haltConsumed(flags.haltConsumption) && (!final || haltOwnsAbortedFinal)) {
|
|
107
108
|
terminal = { kind: "completed" };
|
|
108
109
|
}
|
|
109
110
|
else if (!final) {
|
|
@@ -155,7 +156,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
155
156
|
const result = text;
|
|
156
157
|
let salvagedOutput;
|
|
157
158
|
const { terminal, apiFailure } = terminalCauseOf(final, stats, flags);
|
|
158
|
-
const {
|
|
159
|
+
const { errorCode } = terminalProjection(terminal);
|
|
159
160
|
if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
|
|
160
161
|
salvagedOutput = text.trim() || undefined;
|
|
161
162
|
}
|
|
@@ -164,6 +165,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
164
165
|
void _internalCompaction;
|
|
165
166
|
if (flags.unpricedSpend)
|
|
166
167
|
delete publicStats.costMicroUsd;
|
|
167
|
-
const stampHaltedByUser = flags.
|
|
168
|
+
const stampHaltedByUser = haltConsumed(flags.haltConsumption);
|
|
168
169
|
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
169
170
|
}
|
|
@@ -30,96 +30,32 @@ import type { Prepared } from "./contracts.js";
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildWorkingFileAttachments(spec: TaskSpec, prepared: Prepared): MaybeCompactOptions["workingFileAttachments"];
|
|
32
32
|
/**
|
|
33
|
-
* `MaybeCompactOptions.contextInstructionFiles`: the
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* standing instructions every other turn has been reading — and it loses them exactly where the
|
|
37
|
-
* summary REPLACES the transcript that carried them, so the loss is durable.
|
|
33
|
+
* `MaybeCompactOptions.contextInstructionFiles`: the deployment's instruction-file content (the CLAUDE.md
|
|
34
|
+
* family) delivered to the SUMMARIZER, assembled once here and spread on all three lanes — the compaction
|
|
35
|
+
* boundary is where the summary REPLACES the transcript that carried those instructions.
|
|
38
36
|
*
|
|
39
|
-
*
|
|
40
|
-
* the RAW loader text
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* "repository-controlled DATA, not instructions to obey", and this seat hands the same bytes to the
|
|
62
|
-
* summarizer with instruction standing for the summarization task. Delivery THROUGH a host callback
|
|
63
|
-
* is not host AUTHORSHIP: a prior agent, or anyone who can land a commit, can write a
|
|
64
|
-
* "## Compact Instructions" section, and the mint's neutralization stops tag breakout — not a
|
|
65
|
-
* sentence asking for a fact to be left out of the summary that replaces the transcript.
|
|
66
|
-
* Why it is nevertheless this value: the seat's own contract names this path ("hosts pass the same
|
|
67
|
-
* instruction-file content their prompt assembly mounts... on the runner path"), the parity form this
|
|
68
|
-
* channel exists to reach reads the user's and the project's instruction files at every boundary with
|
|
69
|
-
* exactly this standing, and the two alternatives are worse for reasons above rather than better.
|
|
70
|
-
* The standing is also narrower than the framing gap suggests — the section's preamble scopes it to
|
|
71
|
-
* guidance about HOW to summarize and classifies the rest as background — and a deployment that does
|
|
72
|
-
* not want it has two exits that need no code: do not wire the loader, or return content it vouches
|
|
73
|
-
* for. Recorded as an open design candidate rather than built here, because it is a contract change
|
|
74
|
-
* and not a wiring one: a SEPARATE deployment-vouched summarization-instructions seat, leaving
|
|
75
|
-
* repository-derived context at data tier.
|
|
76
|
-
*
|
|
77
|
-
* RULING ① — BOTH summary forms, no per-form split. Fork eligibility is decided per pass INSIDE
|
|
78
|
-
* maybeCompact (main-model-only, recorded-request-present, not lossy), so a call site cannot say
|
|
79
|
-
* "independent form only"; the closest approximation — pass the seat only when `prepared.compModel`
|
|
80
|
-
* is set, since an independent compaction model has no main prefix to fork — leaves the hole the
|
|
81
|
-
* wrong way round: the configuration it does NOT cover (no compaction model) is precisely the one
|
|
82
|
-
* where a fork degrades mid-pass to the independent form, which would then run WITHOUT the files.
|
|
83
|
-
* Feeding both forms closes that case by construction, and the library face already handles the fork
|
|
84
|
-
* half (the section rides the APPENDED instruction message, never the cached prefix, so the fork's
|
|
85
|
-
* whole economic point is untouched).
|
|
86
|
-
*
|
|
87
|
-
* RULING ② — no new size knob. The bound is the library's own `fitContextInstructionFilesSection`
|
|
88
|
-
* (independent form: cap against the compaction model's window, disclosed elision, and a complete
|
|
89
|
-
* yield when not even the framing fits). Its three KNOWN LIMITS are accepted here KNOWINGLY, as the
|
|
90
|
-
* pre-condition of this wiring rather than something it discovered: a token-dense payload (CJK,
|
|
91
|
-
* emoji) can pass a chars-domain fit and still overflow the gateway's tokenizer; an unknown
|
|
92
|
-
* `contextWindow` leaves the section unbounded (the conversation clamp declines to guess there too);
|
|
93
|
-
* and the FORK form cannot fit at all — its bound is a prompt-too-long that degrades ONCE to the
|
|
94
|
-
* independent form, which the engine's own note records as real but NOT total (an oversize rejection
|
|
95
|
-
* the classifier cannot read as prompt-too-long returns terminally instead).
|
|
96
|
-
*
|
|
97
|
-
* HOW BIG THAT ACCEPTANCE ACTUALLY IS — measured, because the sentence above is easy to read more
|
|
98
|
-
* comfortably than it deserves. The fitted form is NOT the default path: when a fork is eligible and
|
|
99
|
-
* the model answers in its envelope, the fork IS the pass, and its section is delivered WHOLE at any
|
|
100
|
-
* size. The independent form — the only one that fits — runs when there is no fork to take or the
|
|
101
|
-
* fork's answer did not conform. Both facts are pinned in the REF-A2 suite (a 55 KB payload: whole on
|
|
102
|
-
* the fork, elided on the independent fallback). And when a limit does bite, the failure is not a
|
|
103
|
-
* quiet degradation: the section is a FIXED part of the request, the reactive retry loop may drop
|
|
104
|
-
* only CONVERSATION groups, so a request the SECTION overflowed cannot converge and the boundary
|
|
105
|
-
* fails — the task loses that compaction (or that prompt-too-long recovery, or its end-of-task
|
|
106
|
-
* summary) rather than sending a shorter one. The exposure is what the wiring widened: this was
|
|
107
|
-
* previously reachable only by hosts calling the library directly, and is now reachable by every
|
|
108
|
-
* deployment that wires `loadProjectMemory` with a large instruction file and an unknown or small
|
|
109
|
-
* window. Making the section participate in the retry loop is the structural answer; it is a change
|
|
110
|
-
* to that loop's contract, tracked at the library face, and a runner-side knob would only move the
|
|
111
|
-
* decision to a place with less information.
|
|
112
|
-
*
|
|
113
|
-
* WHAT IT COSTS, measured rather than rounded: every deployment that wires `loadProjectMemory` now
|
|
114
|
-
* sends its instruction-file content plus EXACTLY 716 chars of framing (the preamble and the fence)
|
|
115
|
-
* on EVERY summarization request, both forms, once per pass — where a real project instruction file
|
|
116
|
-
* in this family runs to tens of KB, so the payload, not the framing, is the bill. The number is
|
|
117
|
-
* pinned in the REF-A2 suite against the engine's own renderer, so a preamble edit shows up as a
|
|
118
|
-
* changed assertion rather than as prose drifting away from the code it describes.
|
|
119
|
-
* Deployments that never wired the loader pay nothing: the key is absent, not
|
|
120
|
-
* `undefined`, so their requests are byte-identical to the pre-wiring shape on the independent and
|
|
121
|
-
* UPDATE passes (the FORK form appends its ~860-char authority clause with or without the seat, and
|
|
122
|
-
* always did).
|
|
37
|
+
* Contract:
|
|
38
|
+
* - Bytes = `Prepared.projectInstructionContent`, the RAW loader text captured under the same non-blank
|
|
39
|
+
* predicate that composes the project layer. Not the assembled `memoryBlock` (model-authored in-band
|
|
40
|
+
* bytes would gain an instructing envelope), not the COMPOSED project block (`contextInstructionFilesSection`
|
|
41
|
+
* neutralizes the whole authority family, `user_memory`/`scope` included), not `instructionSources`
|
|
42
|
+
* (path + hash only). Raw text in, one fence at the mint.
|
|
43
|
+
* - Provenance: REPOSITORY-CONTROLLED text delivered through a host callback — delivery is not authorship;
|
|
44
|
+
* the mint's neutralization stops tag breakout, not a sentence asking for an omission. Two no-code exits
|
|
45
|
+
* for a deployment that does not want it: do not wire the loader, or return vouched content. A separate
|
|
46
|
+
* deployment-vouched summarization-instructions seat is an open design candidate, not built here.
|
|
47
|
+
* - Both summary forms, no per-form split: fork eligibility is decided per pass inside maybeCompact, and the
|
|
48
|
+
* form NOT covered by a `compModel`-gated split is exactly the fork that degrades mid-pass to the
|
|
49
|
+
* independent form; the section rides the APPENDED instruction message, never the cached prefix.
|
|
50
|
+
* - No new size knob: the bound is `fitContextInstructionFilesSection` (independent form only). Its three
|
|
51
|
+
* known limits are accepted knowingly: a token-dense payload can pass a chars-domain fit and still overflow;
|
|
52
|
+
* an unknown `contextWindow` leaves the section unbounded; the FORK form cannot fit and degrades ONCE. When
|
|
53
|
+
* a limit bites the boundary FAILS (the section is a fixed request part the retry loop cannot drop) — the
|
|
54
|
+
* task loses that compaction rather than sending a shorter one; making the section participate in the
|
|
55
|
+
* retry loop is a library-face contract change, tracked there.
|
|
56
|
+
* - Cost: instruction-file content + exactly 716 chars of framing on every summarization request, both
|
|
57
|
+
* forms, once per pass (pinned in the REF-A2 suite). Deployments that never wired the loader pay nothing:
|
|
58
|
+
* the key is ABSENT, so their requests are byte-identical to the pre-wiring shape.
|
|
123
59
|
*/
|
|
124
60
|
export declare function contextInstructionFilesOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "contextInstructionFiles">>;
|
|
125
61
|
/**
|