@sema-agent/server 7.100.0 → 7.101.0
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/MIGRATION.md +22 -1
- package/README.md +12 -1
- package/README.zh-CN.md +9 -1
- package/USAGE.md +80 -9
- package/dist/approval-card.d.ts +24 -12
- package/dist/approval-card.js +4 -3
- package/dist/approval-policy-settlement.d.ts +31 -0
- package/dist/approval-policy-settlement.js +16 -0
- package/dist/approval-reconciler.d.ts +8 -1
- package/dist/approval-reconciler.js +5 -4
- package/dist/boot/config-center.js +86 -20
- package/dist/boot/execution-env.js +20 -2
- package/dist/boot/parked-revive-gate.js +1 -0
- package/dist/boot/reapers.js +1 -0
- package/dist/boot/resolve-spec.js +25 -8
- package/dist/boot/runner-deps.js +9 -3
- package/dist/boot/stage-02-registry-pull.js +9 -0
- package/dist/boot/stores.js +4 -0
- package/dist/config-catalog.js +5 -3
- package/dist/config-center/apply-effective.d.ts +25 -0
- package/dist/config-center/apply-effective.js +144 -69
- package/dist/config-center/apply-ledger.d.ts +12 -2
- package/dist/config-center/apply-ledger.js +4 -1
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/http-client.d.ts +4 -1
- package/dist/config-center/http-client.js +15 -10
- package/dist/config-center/read-warnings.d.ts +115 -6
- package/dist/config-center/read-warnings.js +115 -5
- package/dist/config-center/types.d.ts +6 -0
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +15 -0
- package/dist/config-provider.d.ts +4 -0
- package/dist/config-provider.js +64 -9
- package/dist/config-types.d.ts +37 -8
- package/dist/config.js +87 -22
- package/dist/deployment-governance.d.ts +45 -12
- package/dist/deployment-governance.js +37 -9
- package/dist/env-name-allowlist-knobs.d.ts +37 -0
- package/dist/env-name-allowlist-knobs.js +24 -0
- package/dist/fleet-client.d.ts +2 -1
- package/dist/hooks/hook-llm.js +13 -4
- package/dist/host-lsp-manager.d.ts +14 -2
- package/dist/host-lsp-manager.js +7 -6
- package/dist/http/admission.js +22 -7
- package/dist/http/memory-store-refusal-reply.d.ts +24 -0
- package/dist/http/memory-store-refusal-reply.js +10 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +3 -3
- package/dist/http/routes/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +3 -5
- package/dist/http/routes/memory-origin.js +3 -5
- package/dist/http/routes/sessions.js +7 -6
- package/dist/http/routes/side-query.js +10 -4
- package/dist/instruction-source-notice.d.ts +97 -0
- package/dist/instruction-source-notice.js +78 -0
- package/dist/memory-layer-legacy-lock.d.ts +14 -0
- package/dist/memory-layer-legacy-lock.js +74 -0
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-store-refusal.d.ts +37 -0
- package/dist/memory-store-refusal.js +14 -0
- package/dist/model-entry-refusal.d.ts +130 -0
- package/dist/model-entry-refusal.js +157 -0
- package/dist/model-provider.d.ts +9 -3
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-select.d.ts +49 -4
- package/dist/model-select.js +48 -17
- package/dist/observability/fail-open.d.ts +9 -1
- package/dist/observability/fail-open.js +9 -1
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +27 -4
- package/dist/observability/secret-env-scrub.js +22 -7
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/local-session-store.d.ts +20 -25
- package/dist/plugins/local-session-store.js +111 -64
- package/dist/plugins/pg-session-storage.d.ts +7 -0
- package/dist/plugins/pg-session-storage.js +19 -0
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -6
- package/dist/plugins/tidb-pool.js +5 -0
- package/dist/plugins/tidb-session-store.d.ts +13 -0
- package/dist/plugins/tidb-session-store.js +14 -0
- package/dist/plugins/web-search.d.ts +2 -1
- package/dist/plugins/web-search.js +43 -24
- package/dist/project-memory.d.ts +10 -0
- package/dist/project-memory.js +28 -14
- package/dist/run-local.d.ts +17 -15
- package/dist/run-local.js +115 -15
- package/dist/sealed-key.d.ts +5 -2
- package/dist/server-secret-env.d.ts +23 -1
- package/dist/server-secret-env.js +9 -0
- package/dist/tool-approval.d.ts +65 -31
- package/dist/tool-approval.js +96 -49
- package/dist/trace/core-keyset-guard.d.ts +27 -5
- package/dist/trace/engine-notice-wire.d.ts +27 -1
- package/dist/trace/engine-notice-wire.js +8 -0
- package/dist/trace/wire-projection-faces.d.ts +2 -2
- package/dist/trace/wire-projection-faces.js +2 -2
- package/package.json +2 -2
- package/dist/memory-layer-preflight.d.ts +0 -46
- package/dist/memory-layer-preflight.js +0 -121
|
@@ -3,6 +3,7 @@ import { TiDBSessionStorage } from "./tidb-session-storage.js";
|
|
|
3
3
|
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
4
4
|
import { jsonColumnText } from "./sql-json-column.js";
|
|
5
5
|
import { escapeLike } from "./sql-escape.js";
|
|
6
|
+
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
6
7
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
7
8
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
8
9
|
import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
|
|
@@ -183,6 +184,19 @@ export class TiDBSessionStore {
|
|
|
183
184
|
const [r] = await this.pool.query("UPDATE session_meta SET title = ? WHERE session_id = ? AND title IS NULL", [title, sessionId]);
|
|
184
185
|
return r.affectedRows > 0;
|
|
185
186
|
}
|
|
187
|
+
async readInstructionSource(sessionId) {
|
|
188
|
+
const [rows] = await this.pool.query({ sql: "SELECT instruction_source FROM session_meta WHERE session_id = ?", timeout: BASELINE_IO_DEADLINE_MS }, [sessionId]);
|
|
189
|
+
return decodeInstructionSource(rows[0]?.instruction_source);
|
|
190
|
+
}
|
|
191
|
+
async recordInstructionSource(sessionId, expected, source) {
|
|
192
|
+
const next = encodeInstructionSource(source);
|
|
193
|
+
if (expected.recorded) {
|
|
194
|
+
await this.pool.query({ sql: "UPDATE session_meta SET instruction_source = ? WHERE session_id = ? AND instruction_source = ?", timeout: BASELINE_IO_DEADLINE_MS }, [next, sessionId, encodeInstructionSource(expected.source)]);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
await this.pool.query({ sql: "UPDATE session_meta SET instruction_source = ? WHERE session_id = ? AND instruction_source IS NULL", timeout: BASELINE_IO_DEADLINE_MS }, [next, sessionId]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
186
200
|
async fork(sourceId, owner) {
|
|
187
201
|
const [metaRows] = await this.pool.query("SELECT leaf_id FROM session_meta WHERE session_id = ?", [sourceId]);
|
|
188
202
|
if (!metaRows[0])
|
|
@@ -139,7 +139,8 @@ export interface WebSearchBackend {
|
|
|
139
139
|
export declare function setWebSearchBadPayloadObserver(fn: ((provider: string) => void) | undefined): void;
|
|
140
140
|
/**
|
|
141
141
|
* Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key
|
|
142
|
-
* stays captured in this closure — it is never surfaced to the model or the tool args.
|
|
142
|
+
* stays captured in this closure — it is never surfaced to the model or the tool args. Every error a leg throws leaves
|
|
143
|
+
* through {@link redactedBackendFailure} (S-716 ③).
|
|
143
144
|
*/
|
|
144
145
|
export declare function createWebSearchBackend(cfg: WebSearchBackendConfig): WebSearchBackend;
|
|
145
146
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createSearxngSearchBackend } from "@sema-agent/core";
|
|
2
|
-
import { isHierarchicalUrl } from "../trace/redact.js";
|
|
2
|
+
import { isHierarchicalUrl, redactSecrets } from "../trace/redact.js";
|
|
3
3
|
export const WEB_SEARCH_PROVIDERS = ["brave", "tavily", "searxng"];
|
|
4
4
|
function isWebSearchProvider(word) {
|
|
5
5
|
return WEB_SEARCH_PROVIDERS.includes(word);
|
|
@@ -80,7 +80,7 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
|
|
|
80
80
|
body.include_domains = opts.allowedDomains.slice(0, 300);
|
|
81
81
|
if (opts?.blockedDomains?.length)
|
|
82
82
|
body.exclude_domains = opts.blockedDomains.slice(0, 150);
|
|
83
|
-
const res = await fetchImpl(cfg.endpoint || TAVILY_BASE, {
|
|
83
|
+
const res = await fetchImpl(new URL(cfg.endpoint || TAVILY_BASE), {
|
|
84
84
|
method: "POST",
|
|
85
85
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
86
86
|
body: JSON.stringify(body),
|
|
@@ -116,6 +116,11 @@ async function searxngSearch(fetchImpl, cfg, query, max, signal, timeoutMs, opts
|
|
|
116
116
|
}
|
|
117
117
|
return normalize(rows, (r) => ({ title: r.title, url: r.url, snippet: r.snippet }), max, "searxng");
|
|
118
118
|
}
|
|
119
|
+
function redactedBackendFailure(e) {
|
|
120
|
+
const text = e instanceof Error ? e.message : String(e);
|
|
121
|
+
const safe = redactSecrets(text);
|
|
122
|
+
return safe === text ? e : new Error(safe);
|
|
123
|
+
}
|
|
119
124
|
export function createWebSearchBackend(cfg) {
|
|
120
125
|
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
121
126
|
const maxResults = Math.max(1, Math.min(20, cfg.maxResults ?? DEFAULT_MAX_RESULTS));
|
|
@@ -132,6 +137,9 @@ export function createWebSearchBackend(cfg) {
|
|
|
132
137
|
case "searxng": return await searxngSearch(fetchImpl, cfg, q, maxResults, sig, timeoutMs, opts);
|
|
133
138
|
}
|
|
134
139
|
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
throw redactedBackendFailure(e);
|
|
142
|
+
}
|
|
135
143
|
finally {
|
|
136
144
|
done();
|
|
137
145
|
}
|
|
@@ -153,6 +161,7 @@ function judgeText(raw) {
|
|
|
153
161
|
return isBlank(raw) ? ABSENT : { ok: true, value: raw };
|
|
154
162
|
}
|
|
155
163
|
const ENDPOINT_FORM = "must be an absolute URL with a scheme and a host, such as https://search.example.com (a bare host or host:port is not one)";
|
|
164
|
+
const ENDPOINT_USERINFO = "must not carry userinfo (user or user:password before the host)";
|
|
156
165
|
function judgeEndpoint(raw) {
|
|
157
166
|
if (raw === undefined || raw === null)
|
|
158
167
|
return ABSENT;
|
|
@@ -160,7 +169,10 @@ function judgeEndpoint(raw) {
|
|
|
160
169
|
return { ok: false, issue: ENDPOINT_FORM };
|
|
161
170
|
if (isBlank(raw))
|
|
162
171
|
return ABSENT;
|
|
163
|
-
|
|
172
|
+
if (!isHierarchicalUrl(raw))
|
|
173
|
+
return { ok: false, issue: ENDPOINT_FORM };
|
|
174
|
+
const url = new URL(raw);
|
|
175
|
+
return url.username !== "" || url.password !== "" ? { ok: false, issue: ENDPOINT_USERINFO } : { ok: true, value: raw };
|
|
164
176
|
}
|
|
165
177
|
const COUNT_FORM = "must be a number ≥ 1";
|
|
166
178
|
function judgeCount(raw) {
|
|
@@ -173,10 +185,11 @@ function envNumber(raw) {
|
|
|
173
185
|
}
|
|
174
186
|
const SEARXNG_PARAM_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
175
187
|
const SEARXNG_PARAMS_FORM = 'must be an object of parameter name → non-empty string, or a "name=value;name=value" string';
|
|
188
|
+
const paramIssue = (entry, problem) => ({ ok: false, issue: `${SEARXNG_PARAMS_FORM} — entry #${entry} ${problem}` });
|
|
176
189
|
function judgeSearxngParams(raw) {
|
|
177
190
|
if (raw === undefined || raw === null)
|
|
178
191
|
return ABSENT;
|
|
179
|
-
const
|
|
192
|
+
const params = [];
|
|
180
193
|
if (typeof raw === "string") {
|
|
181
194
|
const segments = raw.split(";");
|
|
182
195
|
for (const [i, seg] of segments.entries()) {
|
|
@@ -184,32 +197,33 @@ function judgeSearxngParams(raw) {
|
|
|
184
197
|
continue;
|
|
185
198
|
const eq = seg.indexOf("=");
|
|
186
199
|
if (eq < 0)
|
|
187
|
-
return
|
|
188
|
-
|
|
200
|
+
return paramIssue(i + 1, 'has no "="');
|
|
201
|
+
params.push({ entry: i + 1, name: seg.slice(0, eq).trim(), value: seg.slice(eq + 1).trim() });
|
|
189
202
|
}
|
|
190
203
|
}
|
|
191
204
|
else if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
192
205
|
const entries = Object.entries(raw);
|
|
193
|
-
for (const [k, v] of entries) {
|
|
206
|
+
for (const [i, [k, v]] of entries.entries()) {
|
|
194
207
|
if (typeof v !== "string")
|
|
195
|
-
return
|
|
196
|
-
|
|
208
|
+
return paramIssue(i + 1, "has a value that is not a string");
|
|
209
|
+
params.push({ entry: i + 1, name: k, value: v.trim() });
|
|
197
210
|
}
|
|
198
211
|
}
|
|
199
212
|
else {
|
|
200
213
|
return { ok: false, issue: `${SEARXNG_PARAMS_FORM} — got ${Array.isArray(raw) ? "an array" : `a ${typeof raw}`}` };
|
|
201
214
|
}
|
|
202
|
-
const
|
|
203
|
-
for (const
|
|
204
|
-
if (!SEARXNG_PARAM_NAME.test(
|
|
205
|
-
return
|
|
206
|
-
if (
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
const firstEntryOf = new Map();
|
|
216
|
+
for (const p of params) {
|
|
217
|
+
if (!SEARXNG_PARAM_NAME.test(p.name))
|
|
218
|
+
return paramIssue(p.entry, 'has a name that is not a parameter name (letters, digits, "_", "-", ".")');
|
|
219
|
+
if (p.value === "")
|
|
220
|
+
return paramIssue(p.entry, "has an empty value");
|
|
221
|
+
const first = firstEntryOf.get(p.name);
|
|
222
|
+
if (first !== undefined)
|
|
223
|
+
return paramIssue(p.entry, `repeats the name of entry #${first}`);
|
|
224
|
+
firstEntryOf.set(p.name, p.entry);
|
|
211
225
|
}
|
|
212
|
-
return
|
|
226
|
+
return params.length === 0 ? ABSENT : { ok: true, value: Object.fromEntries(params.map((p) => [p.name, p.value])) };
|
|
213
227
|
}
|
|
214
228
|
function assembleBackendConfig(provider, f) {
|
|
215
229
|
return {
|
|
@@ -248,12 +262,17 @@ export function webSearchConfigFromEnv(env = process.env) {
|
|
|
248
262
|
});
|
|
249
263
|
}
|
|
250
264
|
const SETTINGS_WEB_SEARCH_FORM = "settings.webSearch must be an object { provider, apiKey?, endpoint?, searxngParams?, maxResults? }";
|
|
265
|
+
const providerIssue = (shape) => ({
|
|
266
|
+
ok: false,
|
|
267
|
+
issue: `must name one of ${WEB_SEARCH_PROVIDERS.join(" | ")} (${shape}) — it names where this request's searches go, so an unknown or missing provider is refused rather than replaced by this deployment's own backend`,
|
|
268
|
+
});
|
|
251
269
|
function judgeRequestProvider(raw) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
if (raw === undefined || raw === null)
|
|
271
|
+
return providerIssue("it is missing");
|
|
272
|
+
if (typeof raw !== "string")
|
|
273
|
+
return providerIssue("it is not a string");
|
|
274
|
+
const word = providerWord(raw);
|
|
275
|
+
return isWebSearchProvider(word) ? { ok: true, value: word } : providerIssue("it is not one of them");
|
|
257
276
|
}
|
|
258
277
|
export function judgeRequestWebSearch(raw) {
|
|
259
278
|
if (raw === undefined || raw === null)
|
package/dist/project-memory.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PlatformPath } from "node:path";
|
|
2
2
|
import type { ProjectMemoryLoad } from "@sema-agent/core";
|
|
3
|
+
import { type InstructionSourceSeat } from "./instruction-source-notice.js";
|
|
3
4
|
interface ProjectMemoryCtx {
|
|
4
5
|
cwd: string;
|
|
5
6
|
handsEnabled: boolean;
|
|
@@ -46,6 +47,13 @@ export declare function instructionChainCoordinates(absPath: string, p?: Platfor
|
|
|
46
47
|
* (mid-run file edits surface via the probe as tail attachments, never a silently re-rendered prefix).
|
|
47
48
|
* `phase: "post-compact"` forces a re-read (core re-renders the prefix there anyway). No entry for
|
|
48
49
|
* sessionless one-shot runs. (post-clear never arrives from core — [ref] — so it is not a cache key here.)
|
|
50
|
+
*
|
|
51
|
+
* S-690 — source-change notice: with `instructionSource` wired, every prepare of a non-subagent session that WALKED the
|
|
52
|
+
* instruction chain hands the chain member it selected (`null` = none) to {@link announceInstructionSource}, which
|
|
53
|
+
* compares it with the session's persisted baseline. A prepare stopped by a pre-walk gate (hands-less, a cwd that is not
|
|
54
|
+
* an absolute local directory) is "not applicable", not "none" — it never reaches the mint (O6, see `LoadOutcome`). The member is the one the snapshot carries — a cache hit answers the cached pick, so
|
|
55
|
+
* the notice follows what the model actually reads, not the disk (a same-process edit changes nothing until the
|
|
56
|
+
* snapshot is re-read: restart, post-compact, or a new session).
|
|
49
57
|
*/
|
|
50
58
|
export declare function makeLoadProjectMemory(opts: {
|
|
51
59
|
logger?: {
|
|
@@ -54,6 +62,8 @@ export declare function makeLoadProjectMemory(opts: {
|
|
|
54
62
|
maxBytes?: number;
|
|
55
63
|
/** TEST-ONLY: shrink the snapshot-cache bound so the settle-time trim is observable without 256 sessions. */
|
|
56
64
|
cacheMax?: number;
|
|
65
|
+
/** S-690: the baseline store + notice exit. Absent ⇒ no source-change notice (the session store has no baseline face). */
|
|
66
|
+
instructionSource?: InstructionSourceSeat;
|
|
57
67
|
}): (ctx: ProjectMemoryCtx) => Promise<string | null | ProjectMemoryLoad>;
|
|
58
68
|
/**
|
|
59
69
|
* ② The `RunnerDeps.probeInstructionSources` seam: re-fingerprint the declared instruction files mid-run so
|
package/dist/project-memory.js
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs, constants as fsConstants } from "node:fs";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { createHmac, randomBytes } from "node:crypto";
|
|
4
4
|
import hostPath, { isAbsolute, join } from "node:path";
|
|
5
|
+
import { announceInstructionSource } from "./instruction-source-notice.js";
|
|
5
6
|
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
6
7
|
import { scrubServerSecrets } from "./server-secret-env.js";
|
|
7
8
|
import { redactSecrets } from "./trace/redact.js";
|
|
@@ -41,6 +42,7 @@ function instructionHash(raw) {
|
|
|
41
42
|
return `hmac-sha256:${createHmac("sha256", INSTRUCTION_HASH_KEY).update(raw).digest("hex")}`;
|
|
42
43
|
}
|
|
43
44
|
const ABSENT_HASH = null;
|
|
45
|
+
const NOT_APPLICABLE = { walked: false, answer: null };
|
|
44
46
|
export function instructionChainCoordinates(absPath, p = hostPath) {
|
|
45
47
|
const candidates = INSTRUCTION_FILE_CHAIN.filter((rel) => absPath.endsWith(p.sep + rel.split("/").join(p.sep))).sort((a, b) => b.length - a.length);
|
|
46
48
|
for (const rel of candidates) {
|
|
@@ -72,7 +74,7 @@ function git(root, args, maxBytes) {
|
|
|
72
74
|
const safeArgs = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args];
|
|
73
75
|
const scrubFindings = [];
|
|
74
76
|
const scrubbedEnv = scrubServerSecrets(process.env, scrubFindings);
|
|
75
|
-
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings);
|
|
77
|
+
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings, []);
|
|
76
78
|
return new Promise((res) => {
|
|
77
79
|
execFile("git", safeArgs, { cwd: root, timeout: 5000, maxBuffer: maxBytes, env: { ...scrubbedEnv, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" } }, (err, stdout, stderr) => {
|
|
78
80
|
if (!err)
|
|
@@ -90,17 +92,24 @@ export function makeLoadProjectMemory(opts) {
|
|
|
90
92
|
const gitCap = 8 * 1024;
|
|
91
93
|
const cache = new Map();
|
|
92
94
|
const CACHE_MAX = opts.cacheMax ?? 256;
|
|
95
|
+
const seat = opts.instructionSource;
|
|
93
96
|
return async (ctx) => {
|
|
97
|
+
const outcome = await loadOutcome(ctx);
|
|
98
|
+
if (seat && ctx.sessionId && !ctx.isSubagent && outcome.walked)
|
|
99
|
+
await announceInstructionSource(seat, ctx.sessionId, outcome.instructionFile);
|
|
100
|
+
return outcome.answer;
|
|
101
|
+
};
|
|
102
|
+
async function loadOutcome(ctx) {
|
|
94
103
|
if (!ctx.handsEnabled)
|
|
95
|
-
return
|
|
104
|
+
return NOT_APPLICABLE;
|
|
96
105
|
if (!isAbsolute(ctx.cwd))
|
|
97
|
-
return
|
|
106
|
+
return NOT_APPLICABLE;
|
|
98
107
|
let root;
|
|
99
108
|
try {
|
|
100
109
|
root = await fs.realpath(ctx.cwd);
|
|
101
110
|
}
|
|
102
111
|
catch {
|
|
103
|
-
return
|
|
112
|
+
return NOT_APPLICABLE;
|
|
104
113
|
}
|
|
105
114
|
const cacheKey = ctx.sessionId && !ctx.isSubagent ? `${ctx.sessionId}\u0000${root}` : undefined;
|
|
106
115
|
if (cacheKey) {
|
|
@@ -144,11 +153,11 @@ export function makeLoadProjectMemory(opts) {
|
|
|
144
153
|
});
|
|
145
154
|
}
|
|
146
155
|
return load;
|
|
147
|
-
}
|
|
156
|
+
}
|
|
148
157
|
async function doLoad(ctx, root) {
|
|
149
158
|
const st = await fs.stat(root).catch(() => null);
|
|
150
159
|
if (!st?.isDirectory())
|
|
151
|
-
return
|
|
160
|
+
return NOT_APPLICABLE;
|
|
152
161
|
const claudeTrunc = { v: false };
|
|
153
162
|
let claudeRaw = null;
|
|
154
163
|
let hitFile;
|
|
@@ -202,28 +211,33 @@ export function makeLoadProjectMemory(opts) {
|
|
|
202
211
|
parts.push(`## Working tree\n${status.out.split("\n").slice(0, 40).join("\n")}`);
|
|
203
212
|
gitNarrative = parts.join("\n\n");
|
|
204
213
|
}
|
|
214
|
+
const instructionFile = hitFile ?? null;
|
|
205
215
|
const sections = [];
|
|
206
216
|
if (claude && hitFile)
|
|
207
217
|
sections.push(`# ${hitFile}\n${claude}`);
|
|
208
218
|
if (gitNarrative)
|
|
209
219
|
sections.push(gitNarrative);
|
|
210
220
|
if (sections.length === 0)
|
|
211
|
-
return null;
|
|
221
|
+
return { walked: true, answer: null, instructionFile };
|
|
212
222
|
const redacted = redactSecrets(sections.join("\n\n"));
|
|
213
223
|
const capped = capUtf8(redacted, totalCap);
|
|
214
224
|
opts.logger?.debug?.("project_memory_loaded", { cwd: root, bytes: Buffer.byteLength(capped, "utf8"), isSubagent: ctx.isSubagent, gitOk, ...(gitDropReason ? { gitDropReason } : {}), claudeTruncated, ...(hitFile ? { instructionFile: hitFile } : {}), ...(ctx.phase ? { phase: ctx.phase } : {}), cacheSize: cache.size });
|
|
215
225
|
if (!capped)
|
|
216
|
-
return null;
|
|
226
|
+
return { walked: true, answer: null, instructionFile };
|
|
217
227
|
if (hitFile && hitHash) {
|
|
218
228
|
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
walked: true,
|
|
230
|
+
answer: {
|
|
231
|
+
content: capped,
|
|
232
|
+
instructionSources: [
|
|
233
|
+
...INSTRUCTION_FILE_CHAIN.slice(0, hitIndex).map((rel) => ({ path: join(root, rel), contentHash: ABSENT_HASH })),
|
|
234
|
+
{ path: join(root, hitFile), contentHash: hitHash },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
instructionFile,
|
|
224
238
|
};
|
|
225
239
|
}
|
|
226
|
-
return capped;
|
|
240
|
+
return { walked: true, answer: capped, instructionFile };
|
|
227
241
|
}
|
|
228
242
|
}
|
|
229
243
|
export function makeProbeInstructionSources() {
|
package/dist/run-local.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type
|
|
2
|
+
import { type AskRequest, type Brain, type TaskSpec } from "@sema-agent/core";
|
|
3
3
|
import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js";
|
|
4
4
|
import { type ServiceConfig } from "./config.js";
|
|
5
5
|
import { type Logger } from "./observability/logger.js";
|
|
@@ -77,9 +77,12 @@ export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigVie
|
|
|
77
77
|
* 叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE / 守卫集)。本函数一条 policy 都不
|
|
78
78
|
* 自己合成。
|
|
79
79
|
*
|
|
80
|
-
* · **审批基线 =
|
|
81
|
-
*
|
|
82
|
-
* (
|
|
80
|
+
* · **审批基线 = live 席**({@link RUN_LOCAL_APPROVAL_SEAT},S-694):一次性 CLI 没有 durable 审批设施(没有
|
|
81
|
+
* `/decide` 腿可赎回),但**有**一只活 onAsk 席({@link createLocalApprover})。三张审批名单因此与 HTTP 腿
|
|
82
|
+
* **同一只**策略执法(`createDurableAskPolicy`:deny ⇒ 拒;require / never-auto ⇒ ask ⇒ 那只席当场服),
|
|
83
|
+
* 不叠 durable 问答门。修前本腿不传席 ⇒ 落 allow-all 臂 ⇒ 名单在 CLI 上无声空转 —— [ref] 件一把「没有
|
|
84
|
+
* durable 赎回腿」等同成了「ask 无处可服」,可件三同窗装上的正是那只服 ask 的席。运维照旧用
|
|
85
|
+
* `AUTONOMY` / `commandPolicy` / 守卫集在其上收紧。
|
|
83
86
|
* · **裁决 env 恒 host 形,锚 `workspaceDir`**:run-local 的手就跑在这台机器的这个目录上
|
|
84
87
|
* (`--workspace` / config.d workdir / cwd 三选一,调用方已解析好),所以 `cwd` 恒在场 —— 相对形写目标
|
|
85
88
|
* 由 core 按 `rootPath` 解析进真身裁决,构造口的词法补层(沙箱腿那条)在这里自动不铺。
|
|
@@ -122,27 +125,26 @@ export interface LocalApproverIo {
|
|
|
122
125
|
* ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层
|
|
123
126
|
* 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。
|
|
124
127
|
*
|
|
128
|
+
* 🔴 S-694 起本席也服**三张审批名单**产的 ask(审批基线取 live 席,见 {@link RUN_LOCAL_APPROVAL_SEAT}):
|
|
129
|
+
* `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ 策略 `ask` ⇒ 落到这里。`AskRequest` 不带是哪条声明产的
|
|
130
|
+
* ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),所以无 TTY 拒句**列出**可能产门的全部旋钮,
|
|
131
|
+
* `APPROVAL_DENY` 也点名(它当场拒、不经本席 —— 运维排查「为什么这只工具不跑」时要在同一句里看到它)。
|
|
132
|
+
*
|
|
125
133
|
* 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
|
|
126
134
|
* `suspendAsk`**(dist `core/hooks.js` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处
|
|
127
135
|
* 可去(fail-closed 拒),但这一条仍是本席的契约:答案是「不批」,不是「找不到人」。
|
|
128
136
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
* 🔴 决断三臂(codex 验收轮 high,红先修):`readline/promises` 的 `question` 在输入流 EOF/close 时
|
|
134
|
-
* **不决**(promise 悬挂)——Ctrl-D、关闭的 stdin、任务取消都会把 runLocal 永久挂住,文件后端的锁
|
|
135
|
-
* 跟着被拖住,下一次同 root 调用直接卡死。三臂全部 fail-closed 决 false:
|
|
136
|
-
* · `signal` abort(core `OnAsk` 递进来的取消,含预先 aborted);
|
|
137
|
-
* · readline `close`(输入流 end/close 时 readline 自关);
|
|
138
|
-
* · `question` 自身 reject(abort throw / 流错误)。 */
|
|
137
|
+
/** 本席的形:core `OnAsk` 的函数臂(`"allow"` / `"deny"` 两个字面臂本席从不用),答案恒是布尔 —— 结构上可赋给 `OnAsk`。 */
|
|
138
|
+
export type LocalApprover = (req: AskRequest, signal?: AbortSignal) => Promise<boolean>;
|
|
139
|
+
export declare function createLocalApprover(io: LocalApproverIo): LocalApprover;
|
|
139
140
|
export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: {
|
|
140
141
|
input: NodeJS.ReadableStream;
|
|
141
142
|
output: NodeJS.WritableStream;
|
|
142
143
|
}): Promise<boolean>;
|
|
143
144
|
/**
|
|
144
145
|
* Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit
|
|
145
|
-
* code (0 = completed, 1 = the task did not complete
|
|
146
|
+
* code (0 = completed its work, 1 = the task did not complete — including a `completed` run that a person stopped,
|
|
147
|
+
* see {@link personStopOf} — / a fatal error, 2 = bad usage / unmet env). Pure of
|
|
146
148
|
* `process.exit` so a test can assert the code; `main()` applies it.
|
|
147
149
|
*/
|
|
148
150
|
export declare function runLocal(argv: string[], deps?: RunLocalDeps): Promise<number>;
|
package/dist/run-local.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
2
|
+
import { DEFAULT_MODEL_REF, resolveTaskModel } from "./model-select.js";
|
|
3
|
+
import { defaultModelRefusal, modelUnavailableMessage } from "./model-entry-refusal.js";
|
|
3
4
|
import { buildDurableAbsenceNote, createCorruptReadSeat } from "./observability/corrupt-read-seat.js";
|
|
4
5
|
import { outwardPlane } from "./terminal.js";
|
|
5
6
|
import { homedir } from "node:os";
|
|
@@ -8,7 +9,7 @@ import { realpathSync } from "node:fs";
|
|
|
8
9
|
import { createInterface } from "node:readline/promises";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { loadRemoteExec } from "@sema-agent/settings-schema/node";
|
|
11
|
-
import { Runner, uuidv7,
|
|
12
|
+
import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
|
|
12
13
|
import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
|
|
13
14
|
import { createBrain } from "./brain.js";
|
|
14
15
|
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
|
|
@@ -25,6 +26,7 @@ import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario,
|
|
|
25
26
|
import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
26
27
|
import { executionLaneHomeDir, executionLaneOf } from "./execution-lane-caps.js";
|
|
27
28
|
import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
|
|
29
|
+
import { createInstructionSourceSeat } from "./instruction-source-notice.js";
|
|
28
30
|
import { loadDeploymentSkills } from "./capabilities/skills.js";
|
|
29
31
|
import { createRepoClient } from "./capabilities/repo-tools.js";
|
|
30
32
|
import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
|
|
@@ -122,22 +124,81 @@ export function missingGatewayRequirements(config) {
|
|
|
122
124
|
}
|
|
123
125
|
return missing;
|
|
124
126
|
}
|
|
127
|
+
const RUN_LOCAL_APPROVAL_SEAT = { kind: "live" };
|
|
125
128
|
export function applyLocalGovernance(base, config, workspaceDir, engineDataRoot) {
|
|
126
|
-
return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }, { engineDataRoot }));
|
|
129
|
+
return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config, RUN_LOCAL_APPROVAL_SEAT) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }, { engineDataRoot }));
|
|
127
130
|
}
|
|
128
131
|
export function createLocalApprover(io) {
|
|
132
|
+
let tail = Promise.resolve();
|
|
129
133
|
return async (req, signal) => {
|
|
130
134
|
if (!io.isTty) {
|
|
131
|
-
io.printErr(`denied "${req.toolName}" (${req.message}) — this run-local invocation has no TTY, so there is nobody to ask. ` +
|
|
132
|
-
`The gate comes from this deployment's governance knobs:
|
|
133
|
-
`runtime.commandPolicy (config.d/governance.json).
|
|
135
|
+
io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — this run-local invocation has no TTY, so there is nobody to ask. ` +
|
|
136
|
+
`The gate comes from this deployment's governance knobs: APPROVAL_REQUIRE / APPROVAL_NEVER_AUTO (the approval lists; ` +
|
|
137
|
+
`APPROVAL_DENY refuses outright without asking), AUTONOMY, MANUAL_MODE_SHELL_GATE, runtime.commandPolicy (config.d/governance.json). ` +
|
|
138
|
+
`Run it in a terminal to answer interactively, or relax the knob (APPROVAL_AUTO_BUDGET auto-approves that many APPROVAL_REQUIRE calls; ` +
|
|
139
|
+
`a separate .env for this CLI keeps a server's lists off it).`);
|
|
134
140
|
return false;
|
|
135
141
|
}
|
|
136
|
-
|
|
142
|
+
const argsShown = argsDisplayOf(req.args);
|
|
143
|
+
if (!argsShown.ok) {
|
|
144
|
+
io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — its arguments cannot be displayed ` +
|
|
145
|
+
`(${terminalSafeText(argsShown.reason)}), so there is nothing a person could review; refusing instead of asking.`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const prior = tail;
|
|
149
|
+
let release;
|
|
150
|
+
const mine = new Promise((resolve) => {
|
|
151
|
+
release = resolve;
|
|
152
|
+
});
|
|
153
|
+
tail = prior.then(() => mine);
|
|
154
|
+
try {
|
|
155
|
+
if (!(await priorSettledUnlessAborted(prior, signal)))
|
|
156
|
+
return false;
|
|
157
|
+
return await io.askYesNo(buildLocalAskPrompt(req, argsShown.text), signal);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
release();
|
|
161
|
+
}
|
|
137
162
|
};
|
|
138
163
|
}
|
|
139
|
-
|
|
164
|
+
function priorSettledUnlessAborted(prior, signal) {
|
|
140
165
|
if (signal?.aborted)
|
|
166
|
+
return Promise.resolve(false);
|
|
167
|
+
if (signal === undefined)
|
|
168
|
+
return prior.then(() => true);
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const onAbort = () => resolve(false);
|
|
171
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
172
|
+
void prior.then(() => {
|
|
173
|
+
signal.removeEventListener("abort", onAbort);
|
|
174
|
+
resolve(!signal.aborted);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function argsDisplayOf(args) {
|
|
179
|
+
let json;
|
|
180
|
+
try {
|
|
181
|
+
json = JSON.stringify(args);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
185
|
+
}
|
|
186
|
+
if (json === undefined)
|
|
187
|
+
return { ok: false, reason: "the arguments serialize to nothing" };
|
|
188
|
+
return { ok: true, text: terminalSafeText(json) };
|
|
189
|
+
}
|
|
190
|
+
function buildLocalAskPrompt(req, argsShown) {
|
|
191
|
+
return `approve "${terminalSafeText(req.toolName)}"? ${terminalSafeText(req.message)}\n ${argsShown}\n[y/N] `;
|
|
192
|
+
}
|
|
193
|
+
function terminalSafeText(text) {
|
|
194
|
+
return text.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
195
|
+
}
|
|
196
|
+
function inputAlreadyEnded(input) {
|
|
197
|
+
const state = input;
|
|
198
|
+
return state.readableEnded === true || state.destroyed === true;
|
|
199
|
+
}
|
|
200
|
+
export async function promptYesNoOnTty(prompt, signal, streams = { input: process.stdin, output: process.stderr }) {
|
|
201
|
+
if (signal?.aborted || inputAlreadyEnded(streams.input))
|
|
141
202
|
return false;
|
|
142
203
|
const rl = createInterface({ input: streams.input, output: streams.output });
|
|
143
204
|
try {
|
|
@@ -158,6 +219,27 @@ export async function promptYesNoOnTty(prompt, signal, streams = { input: proces
|
|
|
158
219
|
rl.close();
|
|
159
220
|
}
|
|
160
221
|
}
|
|
222
|
+
function personStopOf(result) {
|
|
223
|
+
if (result.terminal.kind !== "completed")
|
|
224
|
+
return undefined;
|
|
225
|
+
if (result.haltedOnUserRejection === true)
|
|
226
|
+
return "haltedOnUserRejection";
|
|
227
|
+
if (result.haltedByUser === true)
|
|
228
|
+
return "haltedByUser";
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
function buildPersonStopFooter(stop, sessionId) {
|
|
232
|
+
switch (stop) {
|
|
233
|
+
case "haltedOnUserRejection":
|
|
234
|
+
return `task stopped (haltedOnUserRejection): a tool call was refused, so the run did not finish its work — continue it with --session ${sessionId}`;
|
|
235
|
+
case "haltedByUser":
|
|
236
|
+
return `task stopped (haltedByUser): the run was halted before it finished its work — continue it with --session ${sessionId}`;
|
|
237
|
+
default: {
|
|
238
|
+
const unhandled = stop;
|
|
239
|
+
throw new Error(`person-stop marker "${String(unhandled)}" has no footer`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
161
243
|
export async function runLocal(argv, deps = {}) {
|
|
162
244
|
const print = deps.print ?? ((l) => console.log(l));
|
|
163
245
|
const printErr = deps.printErr ?? ((l) => console.error(l));
|
|
@@ -195,6 +277,8 @@ export async function runLocal(argv, deps = {}) {
|
|
|
195
277
|
if (r) {
|
|
196
278
|
for (const de of r.domainErrors ?? [])
|
|
197
279
|
logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "domain fell to schema defaults; other domains applied normally" });
|
|
280
|
+
for (const d of r.droppedEntries ?? [])
|
|
281
|
+
logger.warn("config_entry_dropped", { domain: d.domain, entry: d.entry, error: d.error, version: r.effective.version, phase: "boot", note: "this one entry failed its domain schema and was dropped on its own — the rest of the candidate applies (per-entry tolerance); fix the named entry to bring it back" });
|
|
198
282
|
effective = r.effective;
|
|
199
283
|
applyEffective(config, r.effective, logger, { leg: "boot" });
|
|
200
284
|
if (effective?.mcp)
|
|
@@ -208,6 +292,9 @@ export async function runLocal(argv, deps = {}) {
|
|
|
208
292
|
catch (err) {
|
|
209
293
|
logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
|
|
210
294
|
}
|
|
295
|
+
const noDefaultModel = defaultModelRefusal(config, "env");
|
|
296
|
+
if (noDefaultModel !== undefined)
|
|
297
|
+
throw new Error(noDefaultModel);
|
|
211
298
|
const endpointRefusal = modelRouteEndpointRefusal(config);
|
|
212
299
|
if (endpointRefusal !== undefined)
|
|
213
300
|
throw new Error(endpointRefusal);
|
|
@@ -242,7 +329,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
242
329
|
return 2;
|
|
243
330
|
}
|
|
244
331
|
const ungatedCompensation = buildOnlySensitiveBaselineWarning(config, {
|
|
245
|
-
|
|
332
|
+
approvalSeat: RUN_LOCAL_APPROVAL_SEAT.kind,
|
|
246
333
|
singleUserAutoAcceptBaseline: singleUserAutoAcceptBaselineApplies(config),
|
|
247
334
|
});
|
|
248
335
|
if (ungatedCompensation)
|
|
@@ -286,7 +373,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
286
373
|
...(hostRx.commandTimeoutMs != null ? { commandTimeoutMs: hostRx.commandTimeoutMs } : {}),
|
|
287
374
|
})
|
|
288
375
|
: undefined;
|
|
289
|
-
const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? createHostLspManager((event, fields) => logger.info(event, fields)) : undefined;
|
|
376
|
+
const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? createHostLspManager(config, (event, fields) => logger.info(event, fields)) : undefined;
|
|
290
377
|
const memoryEngine = memoryEngineBackendFor(config, root, memoryEngineIncident);
|
|
291
378
|
let memorySync;
|
|
292
379
|
if (config.memorySync && memoryEngine) {
|
|
@@ -417,7 +504,10 @@ export async function runLocal(argv, deps = {}) {
|
|
|
417
504
|
onError: engineOnError,
|
|
418
505
|
onAsk,
|
|
419
506
|
...(config.projectMemoryEnabled
|
|
420
|
-
? {
|
|
507
|
+
? {
|
|
508
|
+
loadProjectMemory: makeLoadProjectMemory({ logger, instructionSource: createInstructionSourceSeat(sessionStore, sharedRunnerDeps.onNotice) }),
|
|
509
|
+
probeInstructionSources: makeProbeInstructionSources(),
|
|
510
|
+
}
|
|
421
511
|
: {}),
|
|
422
512
|
};
|
|
423
513
|
const runner = new Runner(runnerDeps);
|
|
@@ -460,7 +550,14 @@ export async function runLocal(argv, deps = {}) {
|
|
|
460
550
|
}
|
|
461
551
|
throw err;
|
|
462
552
|
}
|
|
463
|
-
const mention =
|
|
553
|
+
const mention = resolveTaskModel(undefined, args.objective, config.models, undefined, config.modelRefusals);
|
|
554
|
+
if (mention.unavailable !== undefined) {
|
|
555
|
+
const { requested, name, refusal } = mention.unavailable;
|
|
556
|
+
printErr(`cannot run: ${modelUnavailableMessage("@-mention", requested, name, refusal)}`);
|
|
557
|
+
void lspManager?.dispose();
|
|
558
|
+
await fileBackend.dispose();
|
|
559
|
+
return 2;
|
|
560
|
+
}
|
|
464
561
|
const scope = args.user ?? "local";
|
|
465
562
|
const taskTimeoutSec = Math.floor(parseNumOrFailNonNegative("TASK_TIMEOUT_SEC", process.env.TASK_TIMEOUT_SEC || "0"));
|
|
466
563
|
const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "discuss");
|
|
@@ -474,13 +571,13 @@ export async function runLocal(argv, deps = {}) {
|
|
|
474
571
|
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
475
572
|
};
|
|
476
573
|
const spec = applyLocalGovernance({
|
|
477
|
-
objective: mention.
|
|
574
|
+
objective: mention.cleanedObjective,
|
|
478
575
|
sessionId,
|
|
479
576
|
backgroundScope: "session",
|
|
480
577
|
oneShot: true,
|
|
481
578
|
principal: scope,
|
|
482
579
|
...(memoryEngine ? { memory: memorySpecForRequest(scope, memoryProjectKeyFor(config, scope, workspaceDir), undefined) } : {}),
|
|
483
|
-
model: mention.model
|
|
580
|
+
model: mention.model,
|
|
484
581
|
getApiKeyAndHeaders: keyResolver,
|
|
485
582
|
tools: cap.tools,
|
|
486
583
|
skills: cap.skills,
|
|
@@ -527,11 +624,14 @@ export async function runLocal(argv, deps = {}) {
|
|
|
527
624
|
await fileBackend.dispose();
|
|
528
625
|
return 1;
|
|
529
626
|
}
|
|
627
|
+
const personStop = personStopOf(result);
|
|
530
628
|
if (args.json) {
|
|
531
629
|
print(JSON.stringify(result, null, 2));
|
|
532
630
|
}
|
|
533
631
|
else if (result.terminal.kind === "completed") {
|
|
534
632
|
print(result.result);
|
|
633
|
+
if (personStop !== undefined)
|
|
634
|
+
printErr(buildPersonStopFooter(personStop, sessionId));
|
|
535
635
|
}
|
|
536
636
|
else {
|
|
537
637
|
const plane = outwardPlane(result.terminal);
|
|
@@ -540,7 +640,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
540
640
|
print(salvage);
|
|
541
641
|
printErr(`task ${plane.status}${plane.errorCode ? ` (${plane.errorCode})` : ""}${plane.errorMessage ? `: ${plane.errorMessage}` : ""}`);
|
|
542
642
|
}
|
|
543
|
-
if (result.terminal.kind !== "completed")
|
|
643
|
+
if (result.terminal.kind !== "completed" || personStop !== undefined)
|
|
544
644
|
exitCode = 1;
|
|
545
645
|
if (memorySync) {
|
|
546
646
|
try {
|
package/dist/sealed-key.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export interface SealedKeyStore extends SealedKeyOpener {
|
|
|
23
23
|
createdAt: string;
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
/** Why a model's sealed key could not be unsealed (the poison-pill diagnosis, see SealedKeyPoison).
|
|
27
|
-
|
|
26
|
+
/** Why a model's sealed key could not be unsealed (the poison-pill diagnosis, see SealedKeyPoison). CUSTODY failures only:
|
|
27
|
+
* a sealedApiKey that is malformed (alg other than the pinned v1 SEALED_KEY_ALG, non-base64 ciphertext) makes the whole
|
|
28
|
+
* catalog entry unavailable instead (S-704 `config.model_entry_invalid`, settings-schema `SealedApiKey` is the shape
|
|
29
|
+
* owner) — the former `unsupported_alg` reason was retired with that move (it could no longer be reached). */
|
|
30
|
+
export type SealedKeyPoisonReason = "sealed_decrypt_failed" | "stale_ciphertext" | "no_key_store";
|
|
28
31
|
/**
|
|
29
32
|
* Poison-pill entry for the per-model key map (`config.modelApiKeys`): planted by applyEffective when
|
|
30
33
|
* a model CARRIES `sealedApiKey` but the ciphertext cannot be unsealed. We deliberately do NOT drop
|