fix(docs): remove stale hashline-{read,edit}.ts rows post-fold
Hashline read/edit tool wrappers were folded into Edit({match}) and
Read({format}) modes in commit ffdec0fee. The two rows in FILE-SYSTEM-MAP.md
pointed to files that no longer exist. Updated the surviving hashline.ts row
to note its new consumer relationship with Edit/Read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
781a7e7319
commit
cc32ab79d9
14 changed files with 625 additions and 9 deletions
|
|
@ -282,9 +282,7 @@
|
||||||
| core/tools/grep.ts | Tool System, File Search | Pattern search tool |
|
| core/tools/grep.ts | Tool System, File Search | Pattern search tool |
|
||||||
| core/tools/ls.ts | Tool System | Directory listing tool |
|
| core/tools/ls.ts | Tool System | Directory listing tool |
|
||||||
| core/tools/truncate.ts | Tool System, Text Processing | Output truncation utility |
|
| core/tools/truncate.ts | Tool System, Text Processing | Output truncation utility |
|
||||||
| core/tools/hashline.ts | Tool System | Hash-based line identification |
|
| core/tools/hashline.ts | Tool System | Hash-based line identification (consumed by `Edit({match: "anchor"})` and `Read({format: "tagged"})` modes since 2026-05-17 fold) |
|
||||||
| core/tools/hashline-read.ts | Tool System | File reading with hash-based line ranges |
|
|
||||||
| core/tools/hashline-edit.ts | Tool System | File editing with hash-based line identification |
|
|
||||||
| core/tools/path-utils.ts | Tool System | Path normalization and validation |
|
| core/tools/path-utils.ts | Tool System | Path normalization and validation |
|
||||||
| core/bash-executor.ts | Tool System | High-level bash execution with event handling |
|
| core/bash-executor.ts | Tool System | High-level bash execution with event handling |
|
||||||
| core/exec.ts | Tool System | Utility functions for command execution |
|
| core/exec.ts | Tool System | Utility functions for command execution |
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,7 @@ export {
|
||||||
type EditToolDetails,
|
type EditToolDetails,
|
||||||
type EditToolInput,
|
type EditToolInput,
|
||||||
type EditToolOptions,
|
type EditToolOptions,
|
||||||
|
createAstGrepTool,
|
||||||
editTool,
|
editTool,
|
||||||
type FindOperations,
|
type FindOperations,
|
||||||
type FindToolDetails,
|
type FindToolDetails,
|
||||||
|
|
@ -345,6 +346,7 @@ export {
|
||||||
getAllToolCompatibility,
|
getAllToolCompatibility,
|
||||||
getToolCompatibility,
|
getToolCompatibility,
|
||||||
grepTool,
|
grepTool,
|
||||||
|
createInsertAroundSymbolTool,
|
||||||
type LsOperations,
|
type LsOperations,
|
||||||
type LsToolDetails,
|
type LsToolDetails,
|
||||||
type LsToolInput,
|
type LsToolInput,
|
||||||
|
|
@ -355,6 +357,7 @@ export {
|
||||||
type ReadToolInput,
|
type ReadToolInput,
|
||||||
type ReadToolOptions,
|
type ReadToolOptions,
|
||||||
readTool,
|
readTool,
|
||||||
|
createReplaceSymbolTool,
|
||||||
registerMcpToolCompatibility,
|
registerMcpToolCompatibility,
|
||||||
// Tool compatibility registry (ADR-005)
|
// Tool compatibility registry (ADR-005)
|
||||||
registerToolCompatibility,
|
registerToolCompatibility,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
import { DispatchLayer } from "../dispatch/dispatch-layer.js";
|
import { DispatchLayer } from "../dispatch/dispatch-layer.js";
|
||||||
import { isInlineEligible } from "../dispatch/run-unit-inline.js";
|
import { isInlineEligible } from "../dispatch/run-unit-inline.js";
|
||||||
import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js";
|
import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js";
|
||||||
|
import { inlineRuntimeGate } from "../uok/inline-runtime-gate.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* #M010/S05: Try inline-scope dispatch via DispatchLayer.
|
* #M010/S05: Try inline-scope dispatch via DispatchLayer.
|
||||||
|
|
@ -53,6 +54,29 @@ import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js";
|
||||||
async function tryInlineDispatch(ctx, s, unitType, unitId, _prompt, options) {
|
async function tryInlineDispatch(ctx, s, unitType, unitId, _prompt, options) {
|
||||||
if (!isInlineEligible(unitType)) return null;
|
if (!isInlineEligible(unitType)) return null;
|
||||||
const basePath = s.basePath ?? ctx.basePath ?? process.cwd();
|
const basePath = s.basePath ?? ctx.basePath ?? process.cwd();
|
||||||
|
const gateResult = inlineRuntimeGate.execute({
|
||||||
|
...ctx,
|
||||||
|
unitType,
|
||||||
|
unitId,
|
||||||
|
basePath,
|
||||||
|
});
|
||||||
|
if (gateResult.outcome !== "pass") {
|
||||||
|
debugLog("runUnit", {
|
||||||
|
phase: "inline-route-refused",
|
||||||
|
unitType,
|
||||||
|
unitId,
|
||||||
|
rationale: gateResult.rationale,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
status: "cancelled",
|
||||||
|
errorContext: {
|
||||||
|
message: `[inline-runtime-gate] ${gateResult.rationale}`,
|
||||||
|
category: "policy",
|
||||||
|
isTransient: false,
|
||||||
|
},
|
||||||
|
_via: "inline-gate",
|
||||||
|
};
|
||||||
|
}
|
||||||
debugLog("runUnit", {
|
debugLog("runUnit", {
|
||||||
phase: "inline-route",
|
phase: "inline-route",
|
||||||
unitType,
|
unitType,
|
||||||
|
|
@ -1258,12 +1282,10 @@ async function runUnitViaSwarm(ctx, _pi, s, unitType, unitId, prompt, options) {
|
||||||
* Default: false (each new unit starts with a clean session).
|
* Default: false (each new unit starts with a clean session).
|
||||||
*/
|
*/
|
||||||
export async function runUnit(ctx, pi, s, unitType, unitId, prompt, options) {
|
export async function runUnit(ctx, pi, s, unitType, unitId, prompt, options) {
|
||||||
// #M010/S05: Inline-scope dispatch is default-on for inline-eligible unit types.
|
// #M010/S05 + R074: inline dispatch is attempted for eligible unit types,
|
||||||
// SF_INLINE_DISPATCH=0 is the escape hatch for operators who need to force the
|
// but the inlineRuntimeGate refuses it until R020 and R066 are validated
|
||||||
// swarm/legacy path. validate-milestone, complete-milestone, and reassess-roadmap
|
// unless SF_INLINE_DISPATCH=1 is set as an audited bypass.
|
||||||
// are inline-eligible (R013). This was the S03 feature-flag path; S05 makes it
|
if (isInlineEligible(unitType)) {
|
||||||
// the default and adds SF_INLINE_DISPATCH=0 as the off-switch.
|
|
||||||
if (isInlineEligible(unitType) && process.env.SF_INLINE_DISPATCH !== "0") {
|
|
||||||
const inline = await tryInlineDispatch(
|
const inline = await tryInlineDispatch(
|
||||||
ctx,
|
ctx,
|
||||||
s,
|
s,
|
||||||
|
|
|
||||||
|
|
@ -438,6 +438,23 @@ export function registerHooks(pi, ecosystemHandlers = []) {
|
||||||
"../self-feedback-drain.js"
|
"../self-feedback-drain.js"
|
||||||
);
|
);
|
||||||
dispatchSelfFeedbackInlineFixIfNeeded(process.cwd(), ctx, pi);
|
dispatchSelfFeedbackInlineFixIfNeeded(process.cwd(), ctx, pi);
|
||||||
|
try {
|
||||||
|
const { drainAdversarialFindingsBridge } = await import(
|
||||||
|
"../safety/adversarial-finding-bridge.js"
|
||||||
|
);
|
||||||
|
const bridged = drainAdversarialFindingsBridge(triage.stillBlocked, {
|
||||||
|
basePath: process.cwd(),
|
||||||
|
});
|
||||||
|
if (bridged > 0) {
|
||||||
|
ctx.ui?.notify?.(
|
||||||
|
`adversarial-finding bridge: smoke_gate quarantine applied for ${bridged} high-severity finding${bridged === 1 ? "" : "s"}.`,
|
||||||
|
"warning",
|
||||||
|
{ noticeKind: NOTICE_KIND.SYSTEM_NOTICE },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* non-fatal — adversarial bridge must never block session start */
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* non-fatal — self-feedback drain must never block session start */
|
/* non-fatal — self-feedback drain must never block session start */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@
|
||||||
export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
||||||
/** Default timeout for the dynamic bash tool (seconds). */
|
/** Default timeout for the dynamic bash tool (seconds). */
|
||||||
export const DEFAULT_BASH_TIMEOUT_SECS = 120;
|
export const DEFAULT_BASH_TIMEOUT_SECS = 120;
|
||||||
|
/**
|
||||||
|
* Maximum output tokens allowed for one adversarial/challenge review turn.
|
||||||
|
*
|
||||||
|
* Purpose: cap adversarial review cost/runaway risk while still allowing a
|
||||||
|
* substantive critique of milestone or slice evidence.
|
||||||
|
*
|
||||||
|
* Consumer: uok/auto-dispatch.js challenge envelopes and
|
||||||
|
* uok/adversarial-budget.js enforcement helpers.
|
||||||
|
*/
|
||||||
|
export const ADVERSARIAL_REVIEW_MAX_TOKENS = 60_000;
|
||||||
// ─── Cache Sizes ──────────────────────────────────────────────────────────────
|
// ─── Cache Sizes ──────────────────────────────────────────────────────────────
|
||||||
/** Max directory-listing cache entries before eviction (#611). */
|
/** Max directory-listing cache entries before eviction (#611). */
|
||||||
export const DIR_CACHE_MAX = 200;
|
export const DIR_CACHE_MAX = 200;
|
||||||
|
|
|
||||||
123
src/resources/extensions/sf/safety/adversarial-finding-bridge.js
Normal file
123
src/resources/extensions/sf/safety/adversarial-finding-bridge.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* adversarial-finding-bridge.js — quarantine smoke_gate on severe findings.
|
||||||
|
*
|
||||||
|
* Purpose: connect high-severity adversarial findings to the existing
|
||||||
|
* smoke_gate quarantine control so a challenge result can stop promotion until
|
||||||
|
* an operator reviews the finding.
|
||||||
|
*
|
||||||
|
* Consumer: session-start self-feedback drain in bootstrap/register-hooks.js.
|
||||||
|
*/
|
||||||
|
import { setExperimentalFlag } from "../experimental.js";
|
||||||
|
import { recordSelfFeedback } from "../self-feedback.js";
|
||||||
|
import { isDbAvailable, listSelfFeedbackEntries } from "../sf-db.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true for adversarial findings severe enough to quarantine smoke_gate.
|
||||||
|
*
|
||||||
|
* Purpose: keep the bridge predicate explicit and testable.
|
||||||
|
*
|
||||||
|
* Consumer: bridgeAdversarialFindingToQuarantine and drainAdversarialFindingsBridge.
|
||||||
|
*/
|
||||||
|
export function isHighSeverityAdversarialFinding(entry) {
|
||||||
|
if (!entry || typeof entry !== "object") return false;
|
||||||
|
const kind = String(entry.kind ?? "");
|
||||||
|
const severity = String(entry.severity ?? "");
|
||||||
|
return (
|
||||||
|
(kind === "adversarial-finding" ||
|
||||||
|
kind.startsWith("adversarial-finding:")) &&
|
||||||
|
severity === "high"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quarantineKeyFor(sourceEntry) {
|
||||||
|
return `quarantine:adversarial-finding:${sourceEntry.id ?? "unknown"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function alreadyQuarantinedByKey(key) {
|
||||||
|
if (!isDbAvailable()) return false;
|
||||||
|
return listSelfFeedbackEntries().some(
|
||||||
|
(entry) =>
|
||||||
|
entry.kind === "smoke-gate-quarantined" &&
|
||||||
|
entry.evidence?.quarantineKey === key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge one high-severity adversarial finding to smoke_gate quarantine.
|
||||||
|
*
|
||||||
|
* Purpose: make adversarial review actionable by flipping the promotion gate
|
||||||
|
* and recording a child self-feedback entry with a durable source reference.
|
||||||
|
*
|
||||||
|
* Consumer: drainAdversarialFindingsBridge.
|
||||||
|
*/
|
||||||
|
export function bridgeAdversarialFindingToQuarantine(entry, opts = {}) {
|
||||||
|
if (!isHighSeverityAdversarialFinding(entry)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "entry is not a high-severity adversarial-finding",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = opts.basePath ?? process.cwd();
|
||||||
|
const quarantineKey = quarantineKeyFor(entry);
|
||||||
|
const alreadyQuarantined =
|
||||||
|
opts.alreadyQuarantined ?? ((key) => alreadyQuarantinedByKey(key));
|
||||||
|
if (alreadyQuarantined(quarantineKey, basePath)) {
|
||||||
|
return { ok: true, quarantined: false, reason: "already-quarantined" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFlag =
|
||||||
|
opts.setExperimentalFlag ??
|
||||||
|
((name, value) => setExperimentalFlag(name, value));
|
||||||
|
const record =
|
||||||
|
opts.recordSelfFeedback ?? ((child, bp) => recordSelfFeedback(child, bp));
|
||||||
|
|
||||||
|
try {
|
||||||
|
setFlag("smoke_gate", false);
|
||||||
|
} catch {
|
||||||
|
/* child entry below still records the attempted quarantine */
|
||||||
|
}
|
||||||
|
|
||||||
|
const childEntry = {
|
||||||
|
kind: "smoke-gate-quarantined",
|
||||||
|
severity: "high",
|
||||||
|
summary: `smoke_gate disabled by adversarial-finding bridge. Source: ${entry.id ?? "unknown"} - ${String(entry.summary ?? "").slice(0, 200)}`,
|
||||||
|
evidence: {
|
||||||
|
sourceEntryId: entry.id,
|
||||||
|
sourceKind: entry.kind,
|
||||||
|
sourceSeverity: entry.severity,
|
||||||
|
quarantineKey,
|
||||||
|
bridgeReason:
|
||||||
|
"high-severity adversarial-finding triggered smoke_gate quarantine",
|
||||||
|
},
|
||||||
|
suggestedFix:
|
||||||
|
"Review the source adversarial-finding entry and decide whether the finding warrants quarantine. Re-enable smoke_gate after the issue is resolved.",
|
||||||
|
};
|
||||||
|
|
||||||
|
let childEntryId;
|
||||||
|
try {
|
||||||
|
childEntryId = record(childEntry, basePath)?.entry?.id;
|
||||||
|
} catch {
|
||||||
|
/* quarantine flag decision already happened */
|
||||||
|
}
|
||||||
|
return { ok: true, quarantined: true, childEntryId };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process blocked self-feedback entries and quarantine severe adversarial finds.
|
||||||
|
*
|
||||||
|
* Purpose: make findings from previous runs effective at the next session
|
||||||
|
* boundary, even if the bridge was not loaded when the finding was filed.
|
||||||
|
*
|
||||||
|
* Consumer: bootstrap/register-hooks.js self-feedback drain.
|
||||||
|
*/
|
||||||
|
export function drainAdversarialFindingsBridge(entries, opts = {}) {
|
||||||
|
if (!Array.isArray(entries)) return 0;
|
||||||
|
let count = 0;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!isHighSeverityAdversarialFinding(entry)) continue;
|
||||||
|
bridgeAdversarialFindingToQuarantine(entry, opts);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
@ -489,6 +489,8 @@ const ALLOWED_KIND_DOMAINS = new Set([
|
||||||
// this, R075 challenge units have no kind to file their findings under
|
// this, R075 challenge units have no kind to file their findings under
|
||||||
// (gap reported by sf-mp9u4i25-fczmcj on 2026-05-17).
|
// (gap reported by sf-mp9u4i25-fczmcj on 2026-05-17).
|
||||||
"adversarial-finding",
|
"adversarial-finding",
|
||||||
|
"adversarial-budget-exceeded",
|
||||||
|
"smoke-gate-quarantined",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const KIND_SEGMENT_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
const KIND_SEGMENT_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* adversarial-budget.test.mjs — R075 adversarial review token budget.
|
||||||
|
*
|
||||||
|
* Purpose: verify challenge review budget calculation and feedback filing
|
||||||
|
* behavior before wiring the cap into dispatch envelopes.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { afterEach, test } from "vitest";
|
||||||
|
import { ADVERSARIAL_REVIEW_MAX_TOKENS } from "../constants.js";
|
||||||
|
import {
|
||||||
|
enforceAdversarialBudget,
|
||||||
|
isAdversarialBudgetExceeded,
|
||||||
|
resolveAdversarialBudget,
|
||||||
|
} from "../uok/adversarial-budget.js";
|
||||||
|
|
||||||
|
let savedBudget;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (savedBudget === undefined) delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
else process.env.SF_ADVERSARIAL_MAX_TOKENS = savedBudget;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveAdversarialBudget_when_env_unset_returns_constant", () => {
|
||||||
|
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
assert.equal(resolveAdversarialBudget(), ADVERSARIAL_REVIEW_MAX_TOKENS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveAdversarialBudget_when_env_positive_uses_override", () => {
|
||||||
|
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
process.env.SF_ADVERSARIAL_MAX_TOKENS = "12345";
|
||||||
|
assert.equal(resolveAdversarialBudget(), 12345);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("isAdversarialBudgetExceeded_when_at_limit_returns_true", () => {
|
||||||
|
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
assert.equal(
|
||||||
|
isAdversarialBudgetExceeded(ADVERSARIAL_REVIEW_MAX_TOKENS),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isAdversarialBudgetExceeded(ADVERSARIAL_REVIEW_MAX_TOKENS - 1),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("enforceAdversarialBudget_when_over_limit_records_feedback", () => {
|
||||||
|
savedBudget = process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
delete process.env.SF_ADVERSARIAL_MAX_TOKENS;
|
||||||
|
const entries = [];
|
||||||
|
const result = enforceAdversarialBudget(
|
||||||
|
ADVERSARIAL_REVIEW_MAX_TOKENS + 1,
|
||||||
|
{ unitId: "challenge-M001", milestoneId: "M001" },
|
||||||
|
{ recordSelfFeedback: (entry) => entries.push(entry) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.shortCircuit, true);
|
||||||
|
assert.equal(result.reason, "adversarial-budget-exceeded");
|
||||||
|
assert.equal(entries.length, 1);
|
||||||
|
assert.equal(entries[0].kind, "adversarial-budget-exceeded");
|
||||||
|
assert.equal(entries[0].evidence.unitId, "challenge-M001");
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* adversarial-finding-bridge.test.mjs — R075 finding-to-quarantine bridge.
|
||||||
|
*
|
||||||
|
* Purpose: verify high-severity adversarial findings disable smoke_gate once
|
||||||
|
* and create a child self-feedback entry with a stable source reference.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "vitest";
|
||||||
|
import {
|
||||||
|
bridgeAdversarialFindingToQuarantine,
|
||||||
|
drainAdversarialFindingsBridge,
|
||||||
|
isHighSeverityAdversarialFinding,
|
||||||
|
} from "../safety/adversarial-finding-bridge.js";
|
||||||
|
|
||||||
|
test("isHighSeverityAdversarialFinding_when_high_adversarial_returns_true", () => {
|
||||||
|
assert.equal(
|
||||||
|
isHighSeverityAdversarialFinding({
|
||||||
|
kind: "adversarial-finding:red-team",
|
||||||
|
severity: "high",
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isHighSeverityAdversarialFinding({
|
||||||
|
kind: "adversarial-finding",
|
||||||
|
severity: "medium",
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bridgeAdversarialFindingToQuarantine_when_high_finding_disables_smoke_gate", () => {
|
||||||
|
const flags = [];
|
||||||
|
const entries = [];
|
||||||
|
const result = bridgeAdversarialFindingToQuarantine(
|
||||||
|
{
|
||||||
|
id: "sf-finding-1",
|
||||||
|
kind: "adversarial-finding",
|
||||||
|
severity: "high",
|
||||||
|
summary: "Promotion bypass found.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setExperimentalFlag: (name, value) => flags.push({ name, value }),
|
||||||
|
recordSelfFeedback: (entry) => {
|
||||||
|
entries.push(entry);
|
||||||
|
return { entry: { id: "child-1" } };
|
||||||
|
},
|
||||||
|
alreadyQuarantined: () => false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.quarantined, true);
|
||||||
|
assert.deepEqual(flags, [{ name: "smoke_gate", value: false }]);
|
||||||
|
assert.equal(entries.length, 1);
|
||||||
|
assert.equal(entries[0].kind, "smoke-gate-quarantined");
|
||||||
|
assert.equal(entries[0].evidence.sourceEntryId, "sf-finding-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bridgeAdversarialFindingToQuarantine_when_already_quarantined_is_idempotent", () => {
|
||||||
|
const flags = [];
|
||||||
|
const entries = [];
|
||||||
|
const result = bridgeAdversarialFindingToQuarantine(
|
||||||
|
{
|
||||||
|
id: "sf-finding-1",
|
||||||
|
kind: "adversarial-finding",
|
||||||
|
severity: "high",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setExperimentalFlag: (name, value) => flags.push({ name, value }),
|
||||||
|
recordSelfFeedback: (entry) => entries.push(entry),
|
||||||
|
alreadyQuarantined: () => true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.reason, "already-quarantined");
|
||||||
|
assert.equal(flags.length, 0);
|
||||||
|
assert.equal(entries.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("drainAdversarialFindingsBridge_when_mixed_entries_processes_only_high_findings", () => {
|
||||||
|
const flags = [];
|
||||||
|
const count = drainAdversarialFindingsBridge(
|
||||||
|
[
|
||||||
|
{ id: "one", kind: "adversarial-finding", severity: "high" },
|
||||||
|
{ id: "two", kind: "adversarial-finding", severity: "medium" },
|
||||||
|
{ id: "three", kind: "gap", severity: "high" },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
setExperimentalFlag: (name, value) => flags.push({ name, value }),
|
||||||
|
recordSelfFeedback: () => {},
|
||||||
|
alreadyQuarantined: () => false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(count, 1);
|
||||||
|
assert.deepEqual(flags, [{ name: "smoke_gate", value: false }]);
|
||||||
|
});
|
||||||
115
src/resources/extensions/sf/tests/inline-runtime-gate.test.mjs
Normal file
115
src/resources/extensions/sf/tests/inline-runtime-gate.test.mjs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* inline-runtime-gate.test.mjs — R074 inline dispatch safety gate.
|
||||||
|
*
|
||||||
|
* Purpose: verify inline dispatch fails closed until R020 and R066 are
|
||||||
|
* validated, with SF_INLINE_DISPATCH=1 as the explicit audited bypass.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, beforeEach, describe, test } from "vitest";
|
||||||
|
import { closeDatabase, openDatabase, upsertRequirement } from "../sf-db.js";
|
||||||
|
import { inlineRuntimeGate } from "../uok/inline-runtime-gate.js";
|
||||||
|
|
||||||
|
const roots = [];
|
||||||
|
let savedBypass;
|
||||||
|
|
||||||
|
function makeProject() {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "sf-inline-gate-"));
|
||||||
|
mkdirSync(join(root, ".sf"), { recursive: true });
|
||||||
|
roots.push(root);
|
||||||
|
openDatabase(join(root, ".sf", "sf.db"));
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedRequirement(id, status) {
|
||||||
|
upsertRequirement({
|
||||||
|
id,
|
||||||
|
class: "R",
|
||||||
|
status,
|
||||||
|
description: `Test requirement ${id}`,
|
||||||
|
why: "test",
|
||||||
|
source: "test",
|
||||||
|
primary_owner: "test",
|
||||||
|
supporting_slices: null,
|
||||||
|
validation: null,
|
||||||
|
notes: null,
|
||||||
|
full_content: `# ${id}\n- Status: ${status}`,
|
||||||
|
superseded_by: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
savedBypass = process.env.SF_INLINE_DISPATCH;
|
||||||
|
delete process.env.SF_INLINE_DISPATCH;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
closeDatabase();
|
||||||
|
for (const root of roots.splice(0))
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
if (savedBypass === undefined) delete process.env.SF_INLINE_DISPATCH;
|
||||||
|
else process.env.SF_INLINE_DISPATCH = savedBypass;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("inlineRuntimeGate", () => {
|
||||||
|
test("execute_when_requirements_missing_fails_closed", () => {
|
||||||
|
const root = makeProject();
|
||||||
|
const result = inlineRuntimeGate.execute({
|
||||||
|
basePath: root,
|
||||||
|
unitType: "validate-milestone",
|
||||||
|
unitId: "M001",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.outcome, "fail");
|
||||||
|
assert.equal(result.failureClass, "policy");
|
||||||
|
assert.match(result.rationale, /R020=unknown R066=unknown/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("execute_when_one_requirement_active_fails", () => {
|
||||||
|
const root = makeProject();
|
||||||
|
seedRequirement("R020", "validated");
|
||||||
|
seedRequirement("R066", "active");
|
||||||
|
|
||||||
|
const result = inlineRuntimeGate.execute({
|
||||||
|
basePath: root,
|
||||||
|
unitType: "complete-milestone",
|
||||||
|
unitId: "M001",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.outcome, "fail");
|
||||||
|
assert.match(result.rationale, /R020=validated R066=active/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("execute_when_both_requirements_validated_passes", () => {
|
||||||
|
const root = makeProject();
|
||||||
|
seedRequirement("R020", "validated");
|
||||||
|
seedRequirement("R066", "validated");
|
||||||
|
|
||||||
|
const result = inlineRuntimeGate.execute({
|
||||||
|
basePath: root,
|
||||||
|
unitType: "reassess-roadmap",
|
||||||
|
unitId: "M001/S01",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.outcome, "pass");
|
||||||
|
assert.match(result.rationale, /R020=validated R066=validated/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("execute_when_bypass_set_passes_with_audited_rationale", () => {
|
||||||
|
const root = makeProject();
|
||||||
|
seedRequirement("R020", "active");
|
||||||
|
seedRequirement("R066", "active");
|
||||||
|
process.env.SF_INLINE_DISPATCH = "1";
|
||||||
|
|
||||||
|
const result = inlineRuntimeGate.execute({
|
||||||
|
basePath: root,
|
||||||
|
unitType: "validate-milestone",
|
||||||
|
unitId: "M001",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.outcome, "pass");
|
||||||
|
assert.match(result.rationale, /audited bypass/);
|
||||||
|
});
|
||||||
|
});
|
||||||
86
src/resources/extensions/sf/uok/adversarial-budget.js
Normal file
86
src/resources/extensions/sf/uok/adversarial-budget.js
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* adversarial-budget.js — token budget policy for adversarial review units.
|
||||||
|
*
|
||||||
|
* Purpose: prevent challenge/adversarial review turns from consuming unbounded
|
||||||
|
* tokens while still allowing the review to file structured feedback when the
|
||||||
|
* cap is reached.
|
||||||
|
*
|
||||||
|
* Consumer: uok/auto-dispatch.js challenge dispatch envelopes and future
|
||||||
|
* streaming budget checks.
|
||||||
|
*/
|
||||||
|
import { ADVERSARIAL_REVIEW_MAX_TOKENS } from "../constants.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the effective adversarial review token budget.
|
||||||
|
*
|
||||||
|
* Purpose: allow an explicit operator override without forking dispatch code.
|
||||||
|
*
|
||||||
|
* Consumer: challenge dispatch and focused tests.
|
||||||
|
*/
|
||||||
|
export function resolveAdversarialBudget() {
|
||||||
|
const override = Number(process.env.SF_ADVERSARIAL_MAX_TOKENS ?? "");
|
||||||
|
if (Number.isFinite(override) && override > 0) return Math.floor(override);
|
||||||
|
return ADVERSARIAL_REVIEW_MAX_TOKENS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when the observed token count has reached the adversarial budget.
|
||||||
|
*
|
||||||
|
* Purpose: provide a cheap predicate for both pre-dispatch and streaming checks.
|
||||||
|
*
|
||||||
|
* Consumer: enforceAdversarialBudget.
|
||||||
|
*/
|
||||||
|
export function isAdversarialBudgetExceeded(tokenCount) {
|
||||||
|
return (
|
||||||
|
Number.isFinite(tokenCount) && tokenCount >= resolveAdversarialBudget()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File structured self-feedback and short-circuit when the budget is exceeded.
|
||||||
|
*
|
||||||
|
* Purpose: turn runaway adversarial review into a durable, reviewable feedback
|
||||||
|
* item instead of allowing silent token burn.
|
||||||
|
*
|
||||||
|
* Consumer: future challenge streaming enforcement.
|
||||||
|
*/
|
||||||
|
export function enforceAdversarialBudget(tokenCount, context = {}, opts = {}) {
|
||||||
|
const budget = resolveAdversarialBudget();
|
||||||
|
if (!isAdversarialBudgetExceeded(tokenCount)) return null;
|
||||||
|
|
||||||
|
const { unitId, milestoneId, basePath } = context;
|
||||||
|
const entry = {
|
||||||
|
kind: "adversarial-budget-exceeded",
|
||||||
|
reason: "adversarial-budget-exceeded",
|
||||||
|
severity: "medium",
|
||||||
|
summary: `Adversarial review unit${unitId ? ` "${unitId}"` : ""} exceeded token budget (${tokenCount} >= ${budget}). Review was short-circuited.`,
|
||||||
|
evidence: {
|
||||||
|
tokenCount,
|
||||||
|
budget,
|
||||||
|
...(unitId ? { unitId } : {}),
|
||||||
|
...(milestoneId ? { milestoneId } : {}),
|
||||||
|
},
|
||||||
|
suggestedFix:
|
||||||
|
"Tune the challenge prompt or reduce inlined context so the review fits within the budget. Adjust SF_ADVERSARIAL_MAX_TOKENS only when the higher cap is intentional.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const record =
|
||||||
|
opts.recordSelfFeedback ??
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { recordSelfFeedback } = await import("../self-feedback.js");
|
||||||
|
recordSelfFeedback(entry, basePath ?? process.cwd());
|
||||||
|
} catch {
|
||||||
|
/* feedback filing must never mask the budget decision */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = record(entry, basePath ?? process.cwd());
|
||||||
|
if (result && typeof result.catch === "function") result.catch(() => {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
shortCircuit: true,
|
||||||
|
reason: "adversarial-budget-exceeded",
|
||||||
|
tokenCount,
|
||||||
|
budget,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -115,6 +115,7 @@ import {
|
||||||
decideUnitRuntimeDispatch,
|
decideUnitRuntimeDispatch,
|
||||||
readUnitRuntimeRecord,
|
readUnitRuntimeRecord,
|
||||||
} from "./unit-runtime.js";
|
} from "./unit-runtime.js";
|
||||||
|
import { resolveAdversarialBudget } from "./adversarial-budget.js";
|
||||||
|
|
||||||
const MAX_PARALLEL_RESEARCH_SLICES = 8;
|
const MAX_PARALLEL_RESEARCH_SLICES = 8;
|
||||||
const PARALLEL_RESEARCH_BLOCKING_PHASES = new Set([
|
const PARALLEL_RESEARCH_BLOCKING_PHASES = new Set([
|
||||||
|
|
@ -963,6 +964,7 @@ export const DISPATCH_RULES = [
|
||||||
action: "dispatch",
|
action: "dispatch",
|
||||||
unitType: "challenge",
|
unitType: "challenge",
|
||||||
unitId: challengeUnitId(mid, slice.id),
|
unitId: challengeUnitId(mid, slice.id),
|
||||||
|
maxOutputTokens: resolveAdversarialBudget(),
|
||||||
prompt: await buildChallengePrompt(
|
prompt: await buildChallengePrompt(
|
||||||
mid,
|
mid,
|
||||||
midTitle,
|
midTitle,
|
||||||
|
|
@ -1894,6 +1896,7 @@ export const DISPATCH_RULES = [
|
||||||
action: "dispatch",
|
action: "dispatch",
|
||||||
unitType: "challenge",
|
unitType: "challenge",
|
||||||
unitId: challengeUnitId(mid),
|
unitId: challengeUnitId(mid),
|
||||||
|
maxOutputTokens: resolveAdversarialBudget(),
|
||||||
prompt: await buildChallengePrompt(
|
prompt: await buildChallengePrompt(
|
||||||
mid,
|
mid,
|
||||||
midTitle,
|
midTitle,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { repeatedFeedbackKindGate } from "../detectors/repeated-feedback-kind.js
|
||||||
import { artifactFlapGate } from "../detectors/artifact-flap.js";
|
import { artifactFlapGate } from "../detectors/artifact-flap.js";
|
||||||
import { staleLockGate } from "../detectors/stale-lock.js";
|
import { staleLockGate } from "../detectors/stale-lock.js";
|
||||||
import { periodicDetectorSweepGate } from "../detectors/periodic-runner.js";
|
import { periodicDetectorSweepGate } from "../detectors/periodic-runner.js";
|
||||||
|
import { inlineRuntimeGate } from "./inline-runtime-gate.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gate-registry-bootstrap.js — register ADR-0075 UOK gates that are liftable.
|
* gate-registry-bootstrap.js — register ADR-0075 UOK gates that are liftable.
|
||||||
|
|
@ -41,5 +42,6 @@ registry.register(repeatedFeedbackKindGate);
|
||||||
registry.register(artifactFlapGate);
|
registry.register(artifactFlapGate);
|
||||||
registry.register(staleLockGate);
|
registry.register(staleLockGate);
|
||||||
registry.register(periodicDetectorSweepGate);
|
registry.register(periodicDetectorSweepGate);
|
||||||
|
registry.register(inlineRuntimeGate);
|
||||||
|
|
||||||
export { registry as gateRegistry };
|
export { registry as gateRegistry };
|
||||||
|
|
|
||||||
74
src/resources/extensions/sf/uok/inline-runtime-gate.js
Normal file
74
src/resources/extensions/sf/uok/inline-runtime-gate.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* inline-runtime-gate.js — policy gate for default-on inline dispatch.
|
||||||
|
*
|
||||||
|
* Purpose: refuse inline dispatch until R020 (inline equivalence proof) and
|
||||||
|
* R066 (regression firewall) are validated, unless the operator explicitly
|
||||||
|
* uses the audited `SF_INLINE_DISPATCH=1` bypass.
|
||||||
|
*
|
||||||
|
* Consumer: auto/run-unit.js before DispatchLayer enters the inline execution
|
||||||
|
* path for validate/complete/reassess units.
|
||||||
|
*/
|
||||||
|
import { debugLog } from "../debug-logger.js";
|
||||||
|
import { getRequirementById, isDbAvailable } from "../sf-db.js";
|
||||||
|
|
||||||
|
export const INLINE_RUNTIME_GATE_ID = "inline-runtime-gate";
|
||||||
|
export const INLINE_RUNTIME_GATE_REQUIREMENTS = ["R020", "R066"];
|
||||||
|
|
||||||
|
function readRequirementStatus(id) {
|
||||||
|
if (!isDbAvailable()) return "unknown";
|
||||||
|
try {
|
||||||
|
return getRequirementById(id)?.status ?? "unknown";
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide whether inline dispatch may run for the current unit.
|
||||||
|
*
|
||||||
|
* Purpose: make the unsafe default-on inline path fail closed until its
|
||||||
|
* contract requirements are validated, while retaining an explicit audited
|
||||||
|
* escape hatch for operator-driven development.
|
||||||
|
*
|
||||||
|
* Consumer: tryInlineDispatch in auto/run-unit.js.
|
||||||
|
*/
|
||||||
|
export const inlineRuntimeGate = {
|
||||||
|
id: INLINE_RUNTIME_GATE_ID,
|
||||||
|
type: "policy",
|
||||||
|
execute(ctx = {}) {
|
||||||
|
const statuses = Object.fromEntries(
|
||||||
|
INLINE_RUNTIME_GATE_REQUIREMENTS.map((id) => [
|
||||||
|
id,
|
||||||
|
readRequirementStatus(id),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const allValidated = INLINE_RUNTIME_GATE_REQUIREMENTS.every(
|
||||||
|
(id) => statuses[id] === "validated",
|
||||||
|
);
|
||||||
|
if (allValidated) {
|
||||||
|
return {
|
||||||
|
outcome: "pass",
|
||||||
|
rationale: `inline dispatch allowed: R020=${statuses.R020} R066=${statuses.R066}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.SF_INLINE_DISPATCH === "1") {
|
||||||
|
debugLog(INLINE_RUNTIME_GATE_ID, {
|
||||||
|
event: "audited-bypass",
|
||||||
|
statuses,
|
||||||
|
unitType: ctx.unitType,
|
||||||
|
unitId: ctx.unitId,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
outcome: "pass",
|
||||||
|
rationale: `inline dispatch allowed via audited bypass (SF_INLINE_DISPATCH=1): R020=${statuses.R020} R066=${statuses.R066}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
outcome: "fail",
|
||||||
|
failureClass: "policy",
|
||||||
|
rationale: `inline dispatch refused: R020=${statuses.R020} R066=${statuses.R066}; both must be validated or SF_INLINE_DISPATCH=1 must be set`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue