@sema-agent/core 7.49.0 → 7.49.2
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 +85 -0
- package/dist/core/arg-summary.d.ts +4 -4
- package/dist/core/arg-summary.js +106 -42
- package/dist/core/credential-vocabulary.js +19 -2
- package/dist/core/engine-notice.d.ts +11 -4
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/hooks.d.ts +17 -0
- package/dist/core/runner/prepare-gate-stations.js +2 -2
- package/dist/core/runner/prepare-lsp.d.ts +4 -4
- package/dist/core/runner/prepare-lsp.js +2 -2
- package/dist/core/runner/prepare-safety-scan.js +10 -6
- package/dist/core/runner/run-harness-handlers.js +2 -2
- package/dist/core/runner/tool-turn-repair-seat.d.ts +28 -4
- package/dist/core/runner/tool-turn-repair-seat.js +36 -13
- package/dist/core/runner-deps.d.ts +2 -2
- package/dist/core/shell-wrapper-projection.js +1 -1
- package/dist/core/task-spec.d.ts +2 -2
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/core/tool-conformance.js +2 -1
- package/dist/core/tool-roster.d.ts +11 -6
- package/dist/core/tool-roster.js +8 -4
- package/dist/core/write-protect.d.ts +3 -1
- package/dist/core/write-protect.js +4 -5
- package/dist/tools/fs/bash-program-position.d.ts +2 -1
- package/dist/tools/fs/bash-program-position.js +15 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/bash-readonly-classifier.js +5 -3
- package/dist/tools/fs/program-read-model.d.ts +7 -2
- package/dist/tools/fs/program-read-model.js +21 -1
- package/dist/tools/fs/program-read-model.rows.js +11 -10
- package/dist/tools/fs/read-deny-home.d.ts +5 -1
- package/dist/tools/fs/read-deny-home.js +40 -19
- package/dist/tools/fs/shell-read-check.d.ts +1 -1
- package/dist/tools/fs/shell-read-check.js +43 -14
- package/dist/tools/fs/shell-read-environment.d.ts +74 -0
- package/dist/tools/fs/shell-read-environment.js +71 -0
- package/dist/tools/fs/shell-read-establish.d.ts +8 -4
- package/dist/tools/fs/shell-read-establish.js +34 -36
- package/dist/tools/fs/shell-read-identity.d.ts +3 -2
- package/dist/tools/fs/shell-read-identity.js +5 -3
- package/dist/tools/fs/shell-read-judge.d.ts +69 -15
- package/dist/tools/fs/shell-read-judge.js +30 -10
- package/package.json +1 -1
- package/wire-changes/core@7.49.1.json +57 -0
- package/wire-changes/core@7.49.2.json +138 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { NAMESPACED_NAME_SHAPES, carriesNamespaceSeparator } from "../protocol-table.js";
|
|
2
2
|
import { engineCardTypes, handBandEffects } from "../tool-registry.js";
|
|
3
|
-
import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKeyBoundProblem } from "../tool-roster.js";
|
|
3
|
+
import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKeyBoundProblem, survivorsByName } from "../tool-roster.js";
|
|
4
4
|
import { toolFaceProblem } from "./tool-face-overlay.js";
|
|
5
5
|
import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
|
|
6
6
|
import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
|
|
7
7
|
const REPEAT_RISK = { read: 0, idempotent: 1, write: 2 };
|
|
8
8
|
const leastRepeatSafe = (a, b) => (REPEAT_RISK[a] >= REPEAT_RISK[b] ? a : b);
|
|
9
|
+
const tierOf = (t) => t.irreversibility ?? (t.reversibilityProbe ? "maybe" : undefined);
|
|
9
10
|
export function prepareSafetyScan(input) {
|
|
10
11
|
const { spec, deps } = input;
|
|
11
12
|
const toolEffects = new Map();
|
|
12
|
-
const
|
|
13
|
+
const survivors = survivorsByName(spec.tools ?? []);
|
|
13
14
|
const claimedAliases = new Set();
|
|
14
15
|
const egressTools = new Set();
|
|
15
16
|
const irreversibleTools = new Set();
|
|
@@ -76,10 +77,10 @@ export function prepareSafetyScan(input) {
|
|
|
76
77
|
throw e;
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
if (t.effect) {
|
|
80
|
+
if (t.effect && survivors.get(t.name) === t) {
|
|
80
81
|
toolEffects.set(t.name, t.effect);
|
|
81
82
|
}
|
|
82
|
-
const tier =
|
|
83
|
+
const tier = tierOf(t);
|
|
83
84
|
if (tier !== undefined) {
|
|
84
85
|
irreversibilityTier.set(t.name, tier);
|
|
85
86
|
if (tier === "always" || tier === "maybe") {
|
|
@@ -98,8 +99,11 @@ export function prepareSafetyScan(input) {
|
|
|
98
99
|
egressTools.add(t.name);
|
|
99
100
|
markedBy(t.name, "deployment");
|
|
100
101
|
}
|
|
102
|
+
}
|
|
103
|
+
for (const t of survivors.values()) {
|
|
104
|
+
const tier = tierOf(t);
|
|
101
105
|
for (const alias of t.aliases ?? []) {
|
|
102
|
-
if (alias === t.name ||
|
|
106
|
+
if (alias === t.name || survivors.has(alias))
|
|
103
107
|
continue;
|
|
104
108
|
if (claimedAliases.has(alias))
|
|
105
109
|
continue;
|
|
@@ -122,7 +126,7 @@ export function prepareSafetyScan(input) {
|
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
toolEffects.set(OFFLOAD_TOOL_NAME, "read");
|
|
125
|
-
const claimerEffects = new Map([...claimedAliases].map((a) => [a, toolEffects.get(a) ?? "write"]));
|
|
129
|
+
const claimerEffects = new Map([...claimedAliases, ...survivors.keys()].map((a) => [a, toolEffects.get(a) ?? "write"]));
|
|
126
130
|
const recordHandEffect = (name, effect) => {
|
|
127
131
|
const claimed = claimerEffects.get(name);
|
|
128
132
|
toolEffects.set(name, claimed === undefined ? effect : leastRepeatSafe(claimed, effect));
|
|
@@ -13,7 +13,7 @@ import { announceToolCall, batchResponseDigest, emitSettled } from "./tool-end-b
|
|
|
13
13
|
import { createResponseModelSeat } from "./response-model-seat.js";
|
|
14
14
|
import { createToolCallIdFoldSeat } from "./tool-call-id-fold-seat.js";
|
|
15
15
|
import { createFinishReasonSeat } from "./finish-reason-seat.js";
|
|
16
|
-
import { createToolTurnIncompleteSeat } from "./tool-turn-repair-seat.js";
|
|
16
|
+
import { createToolTurnIncompleteSeat, turnCarriesToolCall } from "./tool-turn-repair-seat.js";
|
|
17
17
|
import { wireOf } from "../tool-name-readings.js";
|
|
18
18
|
import { rosterToolName } from "../tool-roster.js";
|
|
19
19
|
export function createHarnessHandlers(input) {
|
|
@@ -199,7 +199,7 @@ export function createHarnessHandlers(input) {
|
|
|
199
199
|
const content = m.content;
|
|
200
200
|
if (rs.attach.attachState !== undefined)
|
|
201
201
|
rs.counters.cadenceTurns = advanceCadenceClock(rs.attach.attachState, rs.counters.cadenceTurns, content, writeFamilyOf);
|
|
202
|
-
rs.turn.lastTurnHadToolCalls =
|
|
202
|
+
rs.turn.lastTurnHadToolCalls = turnCarriesToolCall(content);
|
|
203
203
|
}
|
|
204
204
|
rs.turn.callStartAt = undefined;
|
|
205
205
|
rs.turn.firstTokenAt = undefined;
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import type { EngineNotice } from "../engine-notice.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type ToolTurnRepair } from "../../internal/llm.js";
|
|
3
3
|
/** The notice's attribution ids — the same pair, for the same reason, as the fold seat's. */
|
|
4
4
|
export interface ToolTurnRepairSeatIds {
|
|
5
5
|
sessionId: string;
|
|
6
6
|
runId: string;
|
|
7
7
|
}
|
|
8
|
+
/** What the message says became of a turn that dropped calls: the run ended on it (`model_output_invalid`), or the next
|
|
9
|
+
* request asks the model to re-issue them. */
|
|
10
|
+
export type ToolTurnOutcome = "terminal" | "reissued";
|
|
11
|
+
/** One thing the person is told, per cause. A dropped-call cause carries the outcome when its message states one (else
|
|
12
|
+
* `undefined`: nothing is said about the run's fate); the others have none. */
|
|
13
|
+
export type ToolTurnIncompleteItem = {
|
|
14
|
+
cause: "truncated";
|
|
15
|
+
count: number;
|
|
16
|
+
outcome: ToolTurnOutcome | undefined;
|
|
17
|
+
} | {
|
|
18
|
+
cause: "unnamed";
|
|
19
|
+
count: number;
|
|
20
|
+
outcome: ToolTurnOutcome | undefined;
|
|
21
|
+
} | {
|
|
22
|
+
cause: "bad_frame";
|
|
23
|
+
count: number;
|
|
24
|
+
} | {
|
|
25
|
+
cause: "missing_result_synthesized";
|
|
26
|
+
count: number;
|
|
27
|
+
toolCallIds: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
/** Does a turn's content carry a tool call? A tool-calling turn GUARANTEES a next request (its results go back to the
|
|
30
|
+
* model): the engine's one continuation fact — the harness's boundary gates and the dropped-call line read it here. */
|
|
31
|
+
export declare function turnCarriesToolCall(content: unknown): boolean;
|
|
8
32
|
/** The person's line for one cause — once per run per cause, whichever observer saw it first. */
|
|
9
33
|
export interface ToolTurnIncompleteSeat {
|
|
10
|
-
/** Announce `
|
|
11
|
-
announce(
|
|
12
|
-
/** Observe one assistant message: announces each stream drop it reports (`streamDropsOf`). */
|
|
34
|
+
/** Announce `item` unless this run already said its cause. */
|
|
35
|
+
announce(item: ToolTurnIncompleteItem): void;
|
|
36
|
+
/** Observe one assistant message: announces each stream drop it reports (`streamDropsOf`), worded by what became of the turn. */
|
|
13
37
|
observe(message: {
|
|
14
38
|
readonly diagnostics?: unknown;
|
|
15
39
|
}): void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { deliverEngineNotice } from "../engine-notice.js";
|
|
2
2
|
import { isProviderEchoShape } from "../../brain/errors.js";
|
|
3
3
|
import { streamDropsOf } from "../../internal/llm.js";
|
|
4
|
+
import { readMember } from "../thrown-value.js";
|
|
5
|
+
import { modelOutputInvalidOfMessage } from "./terminal-account.js";
|
|
4
6
|
const FORM_SENTENCE = {
|
|
5
7
|
trailing_text: "a block after an assistant's tool calls was moved ahead of them",
|
|
6
8
|
late_result: "a result recorded later was moved up behind its call",
|
|
@@ -9,30 +11,51 @@ const FORM_SENTENCE = {
|
|
|
9
11
|
duplicate_call: "a second call with the same id in one assistant message was left out (its results answer the first)",
|
|
10
12
|
orphan_result: "a result that answers no call before it was left out",
|
|
11
13
|
};
|
|
14
|
+
const afterDroppedCalls = (outcome) => outcome === "reissued" ? "; the model was asked to re-issue them" : outcome === "terminal" ? "; the run ended with this turn (model_output_invalid)" : "";
|
|
12
15
|
const INCOMPLETE_SENTENCE = {
|
|
13
|
-
truncated: (
|
|
14
|
-
unnamed: (
|
|
15
|
-
bad_frame: (
|
|
16
|
-
missing_result_synthesized: (
|
|
16
|
+
truncated: (i) => `${i.count} tool call(s) the model made were cut off before their arguments were complete, so they did not run${afterDroppedCalls(i.outcome)}`,
|
|
17
|
+
unnamed: (i) => `${i.count} tool call(s) the model made arrived with no tool name, so they did not run${afterDroppedCalls(i.outcome)}`,
|
|
18
|
+
bad_frame: (i) => `${i.count} frame(s) of the model's streamed response could not be read and were dropped, so the response may be missing content; the model was told`,
|
|
19
|
+
missing_result_synthesized: (i) => `${i.count} earlier tool call(s) have no recorded result, so the model was told their outcome is unknown`,
|
|
17
20
|
};
|
|
21
|
+
export function turnCarriesToolCall(content) {
|
|
22
|
+
try {
|
|
23
|
+
return Array.isArray(content) && content.some((c) => c?.type === "toolCall");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function outcomeOf(message) {
|
|
30
|
+
const stop = readMember(message, "stopReason");
|
|
31
|
+
if (stop === "error")
|
|
32
|
+
return modelOutputInvalidOfMessage(message) !== undefined ? "terminal" : undefined;
|
|
33
|
+
if (stop === "aborted")
|
|
34
|
+
return undefined;
|
|
35
|
+
return turnCarriesToolCall(readMember(message, "content")) || stop === "toolUse" || stop === "length" ? "reissued" : undefined;
|
|
36
|
+
}
|
|
18
37
|
export function createToolTurnIncompleteSeat(onNotice, ids) {
|
|
19
38
|
const announced = new Set();
|
|
20
|
-
const announce = (
|
|
21
|
-
if (announced.has(cause))
|
|
39
|
+
const announce = (item) => {
|
|
40
|
+
if (announced.has(item.cause))
|
|
22
41
|
return;
|
|
23
|
-
announced.add(cause);
|
|
24
|
-
const shown =
|
|
42
|
+
announced.add(item.cause);
|
|
43
|
+
const shown = "toolCallIds" in item ? item.toolCallIds.filter((id) => isProviderEchoShape(id)) : [];
|
|
25
44
|
deliverEngineNotice(onNotice, {
|
|
26
45
|
code: "task.tool_turn_incomplete",
|
|
27
|
-
message: `${INCOMPLETE_SENTENCE[cause](
|
|
28
|
-
detail: { cause, count, ...(shown.length > 0 ? { toolCallIds: shown } : {}), sessionId: ids.sessionId, runId: ids.runId },
|
|
46
|
+
message: `${INCOMPLETE_SENTENCE[item.cause](item)}. Announced once per run for this cause.`,
|
|
47
|
+
detail: { cause: item.cause, count: item.count, ...("outcome" in item && item.outcome !== undefined ? { outcome: item.outcome } : {}), ...(shown.length > 0 ? { toolCallIds: shown } : {}), sessionId: ids.sessionId, runId: ids.runId },
|
|
29
48
|
});
|
|
30
49
|
};
|
|
31
50
|
return {
|
|
32
51
|
announce,
|
|
33
52
|
observe(message) {
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
const drops = streamDropsOf(message);
|
|
54
|
+
if (drops.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const outcome = outcomeOf(message);
|
|
57
|
+
for (const drop of drops)
|
|
58
|
+
announce(drop.cause === "bad_frame" ? { cause: "bad_frame", count: drop.count } : { cause: drop.cause, count: drop.count, outcome });
|
|
36
59
|
},
|
|
37
60
|
};
|
|
38
61
|
}
|
|
@@ -42,7 +65,7 @@ export function createToolTurnRepairSeat(onNotice, ids) {
|
|
|
42
65
|
return (repairs) => {
|
|
43
66
|
const missing = repairs.filter((repair) => repair.form === "missing_result" && repair.beforeRun !== true).flatMap((repair) => repair.toolCallIds);
|
|
44
67
|
if (missing.length > 0)
|
|
45
|
-
incomplete.announce("missing_result_synthesized", missing.length, missing);
|
|
68
|
+
incomplete.announce({ cause: "missing_result_synthesized", count: missing.length, toolCallIds: missing });
|
|
46
69
|
const fresh = repairs.filter((repair) => {
|
|
47
70
|
const key = `${repair.form}\u0000${[...repair.toolCallIds].sort().join("\u0000")}`;
|
|
48
71
|
if (announced.has(key))
|
|
@@ -1249,10 +1249,10 @@ export interface RunnerDeps {
|
|
|
1249
1249
|
* declaration refusing the batch legs). Absent = no posture check anywhere.
|
|
1250
1250
|
*/
|
|
1251
1251
|
interactionPosture?: "interactive" | "headless";
|
|
1252
|
-
/** LSP code-intelligence seam (design/64 §13.1): when set, every task mounts an `lsp` tool routed to this
|
|
1252
|
+
/** LSP code-intelligence seam (design/64 §13.1): when set, every task WITH HANDS mounts an `lsp` tool routed to this
|
|
1253
1253
|
* manager (a deployment-provided language-server bridge, e.g. an E2B LSP sidecar). The manager returns
|
|
1254
1254
|
* `undefined` for a file whose language has no server, and the tool degrades to "fall back to grep". A
|
|
1255
|
-
* task's own `lspManager` overrides this.
|
|
1255
|
+
* task's own `lspManager` overrides this. Neither seat set, or a leg without an execution env ⇒ the engine mounts no `lsp` tool. */
|
|
1256
1256
|
lspManager?: import("./lsp.js").LspServerManager;
|
|
1257
1257
|
/**
|
|
1258
1258
|
* The Artifacts hosting seat — a storage/service-tier seam of the same family as
|
|
@@ -38,7 +38,7 @@ function projectRow(row) {
|
|
|
38
38
|
return executes.length > 0 ? { valueFlags: [], flags: [], commandString: [], executes } : undefined;
|
|
39
39
|
}
|
|
40
40
|
const commandString = sorted(all.filter((o) => o.spec.value?.kind === "shell-text").map((o) => o.spelling));
|
|
41
|
-
const values = all.filter((o) => o.spec.value?.kind !== "shell-text");
|
|
41
|
+
const values = all.filter((o) => o.spec.value?.kind !== "shell-text" && o.spec.mode !== "queries");
|
|
42
42
|
const attached = sorted(values.filter((o) => o.arity === "attached" && !o.spelling.startsWith("--")).map((o) => o.spelling));
|
|
43
43
|
const required = row.launcher.requiredOperands ?? 0;
|
|
44
44
|
const duration = required > 0 && row.launcher.requiredOperandSpelling === DURATION_SPELLING;
|
package/dist/core/task-spec.d.ts
CHANGED
|
@@ -602,8 +602,8 @@ export interface TaskSpec {
|
|
|
602
602
|
* AskUserQuestion tool is mounted (one of the delivery faces in the criterion — see
|
|
603
603
|
* {@link interactiveTools}). Overrides `RunnerDeps.onQuestion`. Distinct from `onAsk` (permission). */
|
|
604
604
|
onQuestion?: import("./ask-question.js").OnQuestion;
|
|
605
|
-
/** LSP code-intelligence seam (design/64 §13.1): when set, the `lsp` tool is mounted and routed to
|
|
606
|
-
* manager. Overrides `RunnerDeps.lspManager`.
|
|
605
|
+
/** LSP code-intelligence seam (design/64 §13.1): when set, the `lsp` tool is mounted on a leg with hands and routed to
|
|
606
|
+
* this manager. Overrides `RunnerDeps.lspManager`. Neither seat set, or no hands ⇒ the engine mounts no `lsp` tool (the model uses grep/read_file). */
|
|
607
607
|
lspManager?: import("./lsp.js").LspServerManager;
|
|
608
608
|
/**
|
|
609
609
|
* design/121 (CC 198 parity): after the agent edits a file, NEW language-server diagnostics are
|
|
@@ -98,7 +98,7 @@ export const TOOL_CATALOG_ENTRIES = [
|
|
|
98
98
|
},
|
|
99
99
|
},
|
|
100
100
|
{ id: "repo-map", name: "RepoMap", source: "builtin", definedIn: "src/tools/fs/repo-map.ts", mountedBy: ["hands"], cards: ["repo-map"], face: { family: "search", effect: "read", contract: c("core.repo_map@1") } },
|
|
101
|
-
{ id: "lsp", name: "LSP", source: "builtin", definedIn: "src/core/lsp.ts", mountedBy: ["
|
|
101
|
+
{ id: "lsp", name: "LSP", source: "builtin", definedIn: "src/core/lsp.ts", mountedBy: ["hands"], cards: [], face: { family: "search", effect: "read", contract: c("core.lsp@1") } },
|
|
102
102
|
{
|
|
103
103
|
id: "artifact", name: ARTIFACT_TOOL_NAME, source: "builtin", definedIn: "src/tools/artifact/artifact-tool.ts", mountedBy: ["optional"], cards: ["artifact"],
|
|
104
104
|
face: { family: "other", effect: "write", contract: c("core.artifact@1"), ruleFace: { primaryParams: ["action"] }, renderHints: { summaryParams: ["action", "file_path", "url"] } },
|
|
@@ -14,6 +14,7 @@ export const TOOL_CONFORMANCE_VECTORS = [
|
|
|
14
14
|
{ name: "SendMessage", why: "needs the runner self seat (a direct prepare has none)" },
|
|
15
15
|
{ name: "AgentTranscript", why: "needs the runner self seat (a direct prepare has none)" },
|
|
16
16
|
{ name: "ListAgents", why: "mounts with the delegation face only under a runner self seat (a direct prepare has none)" },
|
|
17
|
+
{ name: "LSP", why: "needs a wired LSP manager" },
|
|
17
18
|
], [OFFLOAD_READER]),
|
|
18
19
|
vector("hands-readonly", ["always", "hands"], [
|
|
19
20
|
{ name: "Edit", why: "the read-only band mounts the read half only" },
|
|
@@ -23,9 +24,9 @@ export const TOOL_CONFORMANCE_VECTORS = [
|
|
|
23
24
|
{ name: "TaskStop", why: "the background-shell pair mounts on the full shell only" },
|
|
24
25
|
{ name: "EnterWorktree", why: "the worktree pair needs the tracked cwd the full shell owns" },
|
|
25
26
|
{ name: "ExitWorktree", why: "the worktree pair needs the tracked cwd the full shell owns" },
|
|
27
|
+
{ name: "LSP", why: "needs a wired LSP manager" },
|
|
26
28
|
], [OFFLOAD_READER]),
|
|
27
29
|
vector("plan-mode", ["always", "optional"], [
|
|
28
|
-
{ name: "LSP", why: "needs a wired LSP manager" },
|
|
29
30
|
{ name: "Artifact", why: "needs a wired artifact host" },
|
|
30
31
|
{ name: "Skill", why: "needs a skills listing" },
|
|
31
32
|
{ name: "StructuredOutput", why: "needs an output schema" },
|
|
@@ -392,6 +392,11 @@ export declare function ruleFaceOfInstance(schema: TSchema | undefined, declared
|
|
|
392
392
|
primaryParams: string[];
|
|
393
393
|
params: string[];
|
|
394
394
|
};
|
|
395
|
+
/** The instance each NAME dispatches to when several are mounted under it: the LATER mount (the harness keeps the
|
|
396
|
+
* later one). The one selection — the roster's snapshot and every table keyed by a caller tool's name read it here. */
|
|
397
|
+
export declare function survivorsByName<T extends {
|
|
398
|
+
readonly name: string;
|
|
399
|
+
}>(mounted: readonly T[]): Map<string, T>;
|
|
395
400
|
/** What a mount site knows that the instance does not: where the tool came from and which condition fired. */
|
|
396
401
|
export interface RosterMountMeta {
|
|
397
402
|
source: RosterSource;
|
|
@@ -443,13 +448,13 @@ export declare class RosterBuilder {
|
|
|
443
448
|
get tools(): readonly AgentTool[];
|
|
444
449
|
get length(): number;
|
|
445
450
|
metaOf(tool: AgentTool): RosterMountMeta | undefined;
|
|
446
|
-
/** Mount one instance at the tail. An instance that DECLARES an effect writes it into the leg's effect
|
|
447
|
-
* the later mount under a name is the one the harness dispatches to (`snapshot`'s yield rule), so its
|
|
451
|
+
/** Mount one instance at the tail. An engine-lane instance that DECLARES an effect writes it into the leg's effect
|
|
452
|
+
* map — the later mount under a name is the one the harness dispatches to (`snapshot`'s yield rule), so its
|
|
448
453
|
* declaration is the map's truth for that name; a pre-mount registration (the hand band, the plan pair, an
|
|
449
|
-
* MCP axis) stands only for names no mounted instance declares.
|
|
450
|
-
*
|
|
451
|
-
* doors+scan stretch
|
|
452
|
-
*
|
|
454
|
+
* MCP axis) stands only for names no mounted instance declares. The `scenario` lane never writes the map: its
|
|
455
|
+
* declarations were latched by the safety scan BEFORE any mount (on the kept instance of each name, on the
|
|
456
|
+
* synchronous doors+scan stretch — a microtask cannot flip them), so a scenario mount, kept or dropped, leaves
|
|
457
|
+
* the map as the scan made it; an engine lane's mount under that name still displaces it. */
|
|
453
458
|
mount(tool: AgentTool, meta: RosterMountMeta): void;
|
|
454
459
|
/** An ENGINE-INJECTED instance the harness list carries outside the mount array (the ToolSearch lane): a roster
|
|
455
460
|
* row — the model can call it — never a mount-array element (the disclosure lane owns that list and appends the
|
package/dist/core/tool-roster.js
CHANGED
|
@@ -294,6 +294,12 @@ function capRenderHints(h) {
|
|
|
294
294
|
out.approvalCard = h.approvalCard;
|
|
295
295
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
296
296
|
}
|
|
297
|
+
export function survivorsByName(mounted) {
|
|
298
|
+
const winner = new Map();
|
|
299
|
+
for (const t of mounted)
|
|
300
|
+
winner.set(t.name, t);
|
|
301
|
+
return winner;
|
|
302
|
+
}
|
|
297
303
|
export const faceOf = (t) => t;
|
|
298
304
|
function configError(code, message) {
|
|
299
305
|
const e = new Error(message);
|
|
@@ -333,7 +339,7 @@ export class RosterBuilder {
|
|
|
333
339
|
throw configError("config.tool_roster_mount_tag", `tool ${JSON.stringify(tool.name)}: mount tag ${JSON.stringify(meta.mountedBy)} is outside the closed set`);
|
|
334
340
|
this.#meta.set(tool, meta);
|
|
335
341
|
const eff = faceOf(tool).effect;
|
|
336
|
-
if (eff !== undefined &&
|
|
342
|
+
if (eff !== undefined && meta.mountedBy !== "scenario")
|
|
337
343
|
this.#effects.set(tool.name, eff);
|
|
338
344
|
}
|
|
339
345
|
mountAll(tools, meta) {
|
|
@@ -382,9 +388,7 @@ export class RosterBuilder {
|
|
|
382
388
|
}
|
|
383
389
|
snapshot(facts) {
|
|
384
390
|
const all = [...this.#tools, ...this.#engineLane];
|
|
385
|
-
const winner =
|
|
386
|
-
for (const t of all)
|
|
387
|
-
winner.set(t.name, t);
|
|
391
|
+
const winner = survivorsByName(all);
|
|
388
392
|
const entries = [];
|
|
389
393
|
const emitted = new Set();
|
|
390
394
|
for (const t of all) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../internal/harness-types.js";
|
|
2
|
+
import type { ObservedFilesystem } from "../tools/fs/shell-read-environment.js";
|
|
2
3
|
import type { PermissionResult } from "./tool-policy.js";
|
|
3
4
|
import { type ToolCallLike } from "./tool-registry.js";
|
|
4
5
|
/**
|
|
@@ -231,7 +232,8 @@ export interface LegWriteJudgment {
|
|
|
231
232
|
* write rows together — one reading of the target (its spelling, its real target, its symlink chain; a `canonical` key
|
|
232
233
|
* judged as a key), one exempt root, one absence law. `undefined` ⇒ no row covers it. Absent when neither table has a
|
|
233
234
|
* row. */
|
|
234
|
-
readonly judgeWriteTarget: ((target: string, opts
|
|
235
|
+
readonly judgeWriteTarget: ((target: string, opts: {
|
|
236
|
+
readonly fs: ObservedFilesystem;
|
|
235
237
|
readonly signal?: AbortSignal;
|
|
236
238
|
readonly canonical?: true;
|
|
237
239
|
}) => Promise<WriteTargetCoverage | undefined>) | undefined;
|
|
@@ -223,7 +223,7 @@ export function compileLegWriteJudgment(entries, opts) {
|
|
|
223
223
|
judgeWriteTarget: rows.length === 0 && guardRows === undefined
|
|
224
224
|
? undefined
|
|
225
225
|
: async (target, judgeOpts) => {
|
|
226
|
-
const hit = await compiled.judgment(target, { ...(judgeOpts
|
|
226
|
+
const hit = await compiled.judgment(target, { ...(judgeOpts.signal !== undefined ? { signal: judgeOpts.signal } : {}), ...(judgeOpts.canonical === true ? { canonical: true } : {}) }, judgeOpts.fs, guardRows);
|
|
227
227
|
return hit === undefined ? undefined : hit.unreadable === undefined && TABLE_ROWS.has(hit.row) ? "protected" : "guarded";
|
|
228
228
|
},
|
|
229
229
|
};
|
|
@@ -283,9 +283,8 @@ function compileWriteJudgment(rows, origin, opts) {
|
|
|
283
283
|
const base = typeof cwd === "string" && cwd.length > 0 ? cwd : undefined;
|
|
284
284
|
return base !== undefined ? anchorSpelling(pathFamilyOf({ cwd: base }), base, path) : path;
|
|
285
285
|
};
|
|
286
|
-
const env = opts?.env;
|
|
287
286
|
const rootPath = opts?.rootPath;
|
|
288
|
-
const judgment = async (path, ctx, extraSource) => {
|
|
287
|
+
const judgment = async (path, ctx, env, extraSource) => {
|
|
289
288
|
let noted = false;
|
|
290
289
|
const noteAbsence = (cause, unresolvedSymlink) => {
|
|
291
290
|
if (noted)
|
|
@@ -384,13 +383,13 @@ function compileWriteJudgment(rows, origin, opts) {
|
|
|
384
383
|
};
|
|
385
384
|
const matcher = {
|
|
386
385
|
rows,
|
|
387
|
-
targetView: env === undefined ? "spelling-only" : "target",
|
|
386
|
+
targetView: opts?.env === undefined ? "spelling-only" : "target",
|
|
388
387
|
matchPath(path, cwd) {
|
|
389
388
|
const hit = judgePathRows(tableAt([]), { views: [spelledView(anchoredSpelling(path, cwd), "lexical")] }, "tighten");
|
|
390
389
|
return hit === undefined ? null : { name: hit.row.name, kind: hit.row.kind };
|
|
391
390
|
},
|
|
392
391
|
async judge(path, ctx = {}) {
|
|
393
|
-
const hit = await judgment(path, ctx);
|
|
392
|
+
const hit = await judgment(path, ctx, opts?.env);
|
|
394
393
|
if (hit === undefined)
|
|
395
394
|
return { kind: "clear" };
|
|
396
395
|
if (hit.unreadable !== undefined)
|
|
@@ -94,7 +94,8 @@ export interface ProgramPosition {
|
|
|
94
94
|
/** The program the stream-fed chain member runs when the chain names none (`xargs` runs `echo`). */
|
|
95
95
|
readonly defaultProgram?: string;
|
|
96
96
|
/** The program bash RUNS: the first word past the chain. `name` is the folded word as written, `basename` what
|
|
97
|
-
* bash resolves (`/usr/bin/cat` is `cat`). `undefined` when the chain names none
|
|
97
|
+
* bash resolves (`/usr/bin/cat` is `cat`). `undefined` when the chain names none (a lookup, `command -v git`, runs
|
|
98
|
+
* nothing) or the read was refused. */
|
|
98
99
|
readonly program: {
|
|
99
100
|
readonly index: number;
|
|
100
101
|
readonly name: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SHELL_CONTROL_PREFIXES } from "../../shell/lex.js";
|
|
2
2
|
import { shellWordsOf } from "../../shell/shell-words.js";
|
|
3
|
-
import { BLOCK_WORD_NAMES, LAUNCHER_NAMES, feedsArgvFromStdin, launcherOptionMovesBase, launcherOptionStep, launcherRunsShell, programRow, selectedOptions, streamDefaultProgram } from "./program-read-model.js";
|
|
3
|
+
import { BLOCK_WORD_NAMES, LAUNCHER_NAMES, feedsArgvFromStdin, launcherOptionMovesBase, launcherOptionQueries, launcherOptionStep, launcherRunsShell, programRow, selectedOptions, streamDefaultProgram } from "./program-read-model.js";
|
|
4
4
|
export const NOT_AUTO_ALLOWED = "— not auto-allowed";
|
|
5
5
|
export function programBasename(word) {
|
|
6
6
|
return word.includes("/") ? (word.split("/").pop() ?? word) : word;
|
|
@@ -38,7 +38,7 @@ function launcherRowOf(name) {
|
|
|
38
38
|
const row = programRow(name);
|
|
39
39
|
return row?.launcher !== undefined ? row : undefined;
|
|
40
40
|
}
|
|
41
|
-
function pastLauncherOptions(launcher, folded, i, moved) {
|
|
41
|
+
function pastLauncherOptions(launcher, folded, i, moved, queried) {
|
|
42
42
|
const row = launcherRowOf(launcher);
|
|
43
43
|
if (row === undefined)
|
|
44
44
|
return i;
|
|
@@ -57,7 +57,11 @@ function pastLauncherOptions(launcher, folded, i, moved) {
|
|
|
57
57
|
return i;
|
|
58
58
|
if (launcherOptionMovesBase(row, tok))
|
|
59
59
|
moved?.();
|
|
60
|
+
if (launcherOptionQueries(row, tok))
|
|
61
|
+
queried?.();
|
|
60
62
|
i += step;
|
|
63
|
+
if (tok === "--")
|
|
64
|
+
return i;
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
export const PROGRAM_POSITION_REFUSAL_KINDS = Object.freeze(["option_at_program_position", "control_syntax_at_program_position"]);
|
|
@@ -103,12 +107,17 @@ export function readProgramPosition(words) {
|
|
|
103
107
|
let reentryDefault = argvFromStdin ? streamDefaultProgram(basename) : undefined;
|
|
104
108
|
let launcherMovesBase = false;
|
|
105
109
|
const moved = () => { launcherMovesBase = true; };
|
|
110
|
+
let looksUp = false;
|
|
111
|
+
const queried = () => { looksUp = true; };
|
|
106
112
|
const chain = (program, refusal) => {
|
|
107
113
|
const runsDefaultProgram = lastReentry === folded.length - 1;
|
|
108
114
|
return { controlWords, written, assignments, launchers, argvFromStdin, runsDefaultProgram, ...(runsDefaultProgram && reentryDefault !== undefined ? { defaultProgram: reentryDefault } : {}), program, refusal, launcherMovesBase };
|
|
109
115
|
};
|
|
110
116
|
const selectsShell = (l, end) => selectedOptions(launcherRowOf(l.name), folded.slice(l.index + 1, end)).some((o) => o.mode === "interactive");
|
|
111
|
-
|
|
117
|
+
const first = pastLauncherOptions(basename, folded, i + 1, moved, queried);
|
|
118
|
+
if (looksUp)
|
|
119
|
+
return chain(undefined);
|
|
120
|
+
for (let j = first; j < folded.length; j++) {
|
|
112
121
|
const t = folded[j];
|
|
113
122
|
if (owedOperands > 0 && !t.startsWith("-")) {
|
|
114
123
|
owedOperands -= 1;
|
|
@@ -133,7 +142,9 @@ export function readProgramPosition(words) {
|
|
|
133
142
|
if (launcherRunsShell(launcherRowOf(wordBasename), folded.slice(j + 1)))
|
|
134
143
|
return chain(undefined, shellRefusal(j, j + 1));
|
|
135
144
|
owedOperands = lastRow?.requiredOperands ?? 0;
|
|
136
|
-
j = pastLauncherOptions(wordBasename, folded, j + 1, moved) - 1;
|
|
145
|
+
j = pastLauncherOptions(wordBasename, folded, j + 1, moved, queried) - 1;
|
|
146
|
+
if (looksUp)
|
|
147
|
+
return chain(undefined);
|
|
137
148
|
continue;
|
|
138
149
|
}
|
|
139
150
|
if (t.startsWith("-"))
|
|
@@ -167,8 +167,8 @@ export interface StrictResidualOptions {
|
|
|
167
167
|
* 1. the quote-blind class ({@link SHELL_SEGMENT_QUOTE_BLIND_REJECT}) over every written spelling — a word, a
|
|
168
168
|
* redirection target under `tolerateRedirection` — plus every group, here-document, line-break connector
|
|
169
169
|
* and (unless tolerated or data-free) redirection token;
|
|
170
|
-
* 2. the expansion character where bash expands it
|
|
171
|
-
* under `homeDeclared`;
|
|
170
|
+
* 2. the expansion character where bash expands it and a `~user` / `~+` / `~-` tilde word
|
|
171
|
+
* ({@link expandsBeyondText}), the home-directory word excepted under `homeDeclared`;
|
|
172
172
|
* 3. a backgrounding `&` and a `|&`;
|
|
173
173
|
* 4. a word whose value the text does not settle ({@link UNRESOLVED_SENTENCE}) — a compound array, an
|
|
174
174
|
* off-table ANSI-C escape, a subscript or parameter expansion this lexer does not read.
|
|
@@ -26,20 +26,22 @@ function hasBareShellOperator(s, quotedOperatorsAreText) {
|
|
|
26
26
|
for (const t of lexed.segments[0].tokens) {
|
|
27
27
|
if (t.kind !== "word")
|
|
28
28
|
return true;
|
|
29
|
-
if (
|
|
29
|
+
if (expandsBeyondText(t) || t.assignment?.form === "compound")
|
|
30
30
|
return true;
|
|
31
31
|
if (t.pieces.some((p) => p.quotedBy === "\\" || (p.quotedBy === undefined && p.literal !== undefined && /[\r\\]/.test(p.literal))))
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function expandsBeyondText(w) {
|
|
37
37
|
if (w.quotedSpans.some((q) => q.by === '$"'))
|
|
38
38
|
return true;
|
|
39
39
|
for (const p of w.pieces) {
|
|
40
40
|
const k = p.expansion?.kind;
|
|
41
41
|
if (k === "parameter" || k === "command" || k === "process" || k === "arithmetic")
|
|
42
42
|
return true;
|
|
43
|
+
if (k === "tilde" && p.expansion?.body !== "~")
|
|
44
|
+
return true;
|
|
43
45
|
if (p.literal !== undefined && (p.quotedBy === undefined || p.quotedBy === '"') && /[$`]/.test(p.literal))
|
|
44
46
|
return true;
|
|
45
47
|
}
|
|
@@ -126,7 +128,7 @@ export function strictResidualRefusal(lexed, source, opts = {}) {
|
|
|
126
128
|
}
|
|
127
129
|
const home = opts.homeDir !== undefined && !textBindsHome(source) ? opts.homeDir : undefined;
|
|
128
130
|
for (const w of words) {
|
|
129
|
-
if (
|
|
131
|
+
if (expandsBeyondText(w) && !(home !== undefined && homeSpelledForClassify(w, home) !== undefined))
|
|
130
132
|
return reason;
|
|
131
133
|
}
|
|
132
134
|
for (const s of lexed.segments) {
|
|
@@ -29,7 +29,7 @@ export declare const PATH_VALUE_FORMS: readonly ["indirect", "form", "header", "
|
|
|
29
29
|
export type PathValueForm = (typeof PATH_VALUE_FORMS)[number];
|
|
30
30
|
/** What selecting an option does to how the program runs ({@link OptionSpec.mode}) — a CLOSED set (`docs/CLOSED-SETS.md`,
|
|
31
31
|
* `OptionMode`). */
|
|
32
|
-
export declare const OPTION_MODES: readonly ["banner", "interactive", "names-program", "follows", "repeats", "quiet", "forced"];
|
|
32
|
+
export declare const OPTION_MODES: readonly ["banner", "interactive", "names-program", "follows", "repeats", "quiet", "forced", "queries"];
|
|
33
33
|
export type OptionMode = (typeof OPTION_MODES)[number];
|
|
34
34
|
/**
|
|
35
35
|
* What one word is in this program's hands — a CLOSED set (`docs/CLOSED-SETS.md`, `ArgRoleKind`), the eleven members the
|
|
@@ -188,7 +188,8 @@ export interface OptionSpec {
|
|
|
188
188
|
* the program to run without a value (`node --test`; a launcher's, from the caller's environment: `su -m`); `follows`:
|
|
189
189
|
* follows its files as they grow, until killed (`tail -f`); `repeats`: repeats on an interval until killed (`free -s`);
|
|
190
190
|
* `quiet`: prints only what the program's own text tells it to (`sed -n`); `forced`: removes without asking, and passes
|
|
191
|
-
* over an operand that is not there (`rm -f`)
|
|
191
|
+
* over an operand that is not there (`rm -f`); `queries`: a LOOKUP — the operands are names it looks up and describes, and
|
|
192
|
+
* it runs none of them (`command -v`). */
|
|
192
193
|
readonly mode?: OptionMode;
|
|
193
194
|
/** This switch turns the program's own value glob off (`curl -g`): every {@link ArgRole} `programGlob` value of the
|
|
194
195
|
* invocation is read literally. */
|
|
@@ -717,6 +718,10 @@ export declare function launcherOptionPathRole(row: ProgramRow, tok: string): Ex
|
|
|
717
718
|
* written (`nsenter -w` bare takes the target process's directory)? In a cluster, a letter up to the first that takes a
|
|
718
719
|
* value. `false` for any word the walk does not step. */
|
|
719
720
|
export declare function launcherOptionMovesBase(row: ProgramRow, tok: string): boolean;
|
|
721
|
+
/** Does this launcher option word select a LOOKUP ({@link OptionSpec.mode} `queries`: `command -v`, `-V`, a cluster
|
|
722
|
+
* carrying one — `-pv`), so that the words after the options are names and no program is run? In a cluster, a letter up
|
|
723
|
+
* to the first that takes a value. `false` for any word the walk does not step. */
|
|
724
|
+
export declare function launcherOptionQueries(row: ProgramRow, tok: string): boolean;
|
|
720
725
|
/** How many words one launcher option occupies ({@link readLauncherOption}), or `undefined` where the row does not
|
|
721
726
|
* explain it. */
|
|
722
727
|
export declare function launcherOptionStep(row: ProgramRow, tok: string): 1 | 2 | undefined;
|
|
@@ -2,7 +2,7 @@ import { literalTildeSpelling } from "../../shell/lex.js";
|
|
|
2
2
|
import { PROGRAM_ROWS } from "./program-read-model.rows.js";
|
|
3
3
|
export const DATUM_FORMS = Object.freeze(["count", "pattern", "name-pattern", "label", "message", "delimiter"]);
|
|
4
4
|
export const PATH_VALUE_FORMS = Object.freeze(["indirect", "form", "header", "url"]);
|
|
5
|
-
export const OPTION_MODES = Object.freeze(["banner", "interactive", "names-program", "follows", "repeats", "quiet", "forced"]);
|
|
5
|
+
export const OPTION_MODES = Object.freeze(["banner", "interactive", "names-program", "follows", "repeats", "quiet", "forced", "queries"]);
|
|
6
6
|
export const ARG_ROLE_KINDS = Object.freeze(["path", "mount-source", "object-path", "program-text", "program-file", "shell-text", "command", "assignment", "binds-name", "entry-probe", "datum"]);
|
|
7
7
|
export const PATH_EFFECTS = Object.freeze(["read", "names", "write", "create", "edit-in-place"]);
|
|
8
8
|
export const WRITING_EFFECTS = new Set(["write", "create", "edit-in-place"]);
|
|
@@ -1091,6 +1091,26 @@ export function launcherOptionMovesBase(row, tok) {
|
|
|
1091
1091
|
}
|
|
1092
1092
|
return false;
|
|
1093
1093
|
}
|
|
1094
|
+
export function launcherOptionQueries(row, tok) {
|
|
1095
|
+
if (tok === "-" || tok === "--")
|
|
1096
|
+
return false;
|
|
1097
|
+
if (tok.startsWith("--")) {
|
|
1098
|
+
const eq = tok.indexOf("=");
|
|
1099
|
+
return steppableLauncherOption(row, eq > 2 ? tok.slice(0, eq) : tok)?.spec.mode === "queries";
|
|
1100
|
+
}
|
|
1101
|
+
if (!/^-[^-]/.test(tok))
|
|
1102
|
+
return false;
|
|
1103
|
+
for (let i = 1; i < tok.length; i++) {
|
|
1104
|
+
const m = steppableLauncherOption(row, `-${tok[i]}`);
|
|
1105
|
+
if (m === undefined)
|
|
1106
|
+
return false;
|
|
1107
|
+
if (m.spec.mode === "queries")
|
|
1108
|
+
return true;
|
|
1109
|
+
if (m.arity !== 0)
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1094
1114
|
export function launcherOptionStep(row, tok) {
|
|
1095
1115
|
return readLauncherOption(row, tok)?.step;
|
|
1096
1116
|
}
|