@sema-agent/core 5.3.0 → 5.5.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/CHANGELOG.md +31 -0
- package/dist/brain/anthropic.js +12 -2
- package/dist/brain/media-degrade.d.ts +14 -0
- package/dist/brain/media-degrade.js +30 -0
- package/dist/brain/openai.js +9 -11
- package/dist/brain/timeout.d.ts +14 -0
- package/dist/brain/timeout.js +114 -0
- package/dist/core/arg-summary.d.ts +8 -0
- package/dist/core/arg-summary.js +27 -9
- package/dist/core/exec-output-tail.d.ts +0 -1
- package/dist/core/exec-output-tail.js +0 -6
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +12 -4
- package/dist/core/runner/runtask.js +3 -0
- package/dist/core/runner/tool-disclosure.js +29 -2
- package/dist/core/secret-env.d.ts +9 -1
- package/dist/core/secret-env.js +20 -3
- package/dist/core/task-registry-agent.js +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/untrusted-egress.d.ts +2 -1
- package/dist/core/untrusted-egress.js +5 -3
- package/dist/engine/execution-env/node-execution-env.d.ts +0 -1
- package/dist/engine/execution-env/node-execution-env.js +12 -30
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/tools/fs/fs-read.js +5 -3
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +1 -0
- package/dist/tools/scheduler-tools.js +48 -15
- package/dist/tools/web.js +12 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.5.0 (2026-08-03)
|
|
4
|
+
|
|
5
|
+
_Three follow-through items: the deferred-placeholder wrong-guess round trip halves, durable cron jobs survive their creating session, and the redaction findings channel closes its last three audit gaps._
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Deferred-tool placeholders answer a wrong guess with the real schema. A placeholder card carries a name and one hint line, so a model that has to guess a shape copies the local pattern of the sibling tools it can see — and a wrong guess used to cost four rounds (call → a rejection teaching a `ToolSearch` round trip → search → real call). A shape-invalid direct call now comes back as structured invalid-arguments **with the real declared parameter schema inlined**, and activates the tool on the spot — the same activation the `ToolSearch` `select:` lane performs (full schema materialized into the next request, fingerprint/boundary-announce refresh, listing ride), because both lanes run one activation closure. Wrong guess: four rounds → two. Right guess: still zero — the schema-valid direct lane is untouched, the shape gate still refuses to run the real tool body on a call it never declared, and adjudication is unchanged (policy/approval decide before execute, so a denied call activates nothing). Two consequences stated plainly: the rejection is flagged as an error, so a later leg's transcript replay does not credit that activation (the name reverts to a placeholder and the corrected call re-activates it — self-healing, never a widening); and a withdrawn name, or a declaration that cannot be serialized, keeps the previous "activate it first" rejection.
|
|
10
|
+
- Scheduler isolation keys fork by lifetime. A durable cron job was filed under the same key as a session-scoped one — minted `sessionId ?? principal ?? taskId ?? "default"` — so on any host with sessions a durable job belonged to the session that created it: it kept firing, but every later session got `not_found` from `CronList`/`CronDelete` and could not even learn it existed. Durable jobs are now isolated by the stable cross-session identity (`principal`, or a single `"default"` bucket where the deployment carries no principal); session-scoped jobs keep the session chain unchanged. `CronList` reads both lanes (deduplicated, and a lane whose listing fails surfaces the error instead of a half-empty list), `CronDelete` acts on the lane that actually holds the id, and `ScheduleWakeup` stays on the session lane (one session's `stop: true` never touches another's pending wakeup). The cross-tenant refusal is unchanged: another principal's durable job answers `not_found`, never an existence leak. **Deployment note**: durable rows written by earlier versions are still keyed by their originating session — a one-time re-key of existing rows belongs to the scheduler backend; the engine does no migration.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The redaction findings channel completes (external-audit architecture item): findings now carry `source` (a closed provenance vocabulary — `arg-summary` / `untrusted-egress` / `secret-env` — stamped by the minting rule table, with no public finding constructor to forge); `summarizeRedactions(findings)` renders the one-line disclosure a host can announce ("3 redactions: prefixed-token (high) x1, …" — pure function, never auto-injected); and the env arm joins the channel (`scrubSecretEnv(env, findings?)` reports each deleted entry as a `SecretEnvFinding {key, kind, confidence, source}` — deletion, not replacement, so it is its own shape; one summarizer renders both). Absent the optional collector every path is byte-identical to before.
|
|
15
|
+
|
|
16
|
+
## 5.4.0 (2026-08-03)
|
|
17
|
+
|
|
18
|
+
_The external-audit dunning batch closes end-to-end (small cases + all three cleared A-group debts), and the engine grows its outer brain-call guardrail. All additive or louder; major stays locked at 5._
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- webfetch: the tool card follows the wired capability (a mount without a summarizer no longer advertises `prompt` in prose or schema), and an error response stops silently eating the caller's `prompt` (the error leg now carries the same "was NOT applied" disclosure family, naming the HTTP status). Error bodies are deliberately NOT fed to the summarizer (a 404 page dressed up as "the requested analysis" would be worse than the raw dump) — pinned as a negative control.
|
|
23
|
+
- Background-agent poll projection: stop-attribution rides whenever the ROW carries it — the stale-running reaper settles rows as `failed` with `stoppedBy:"system"`, and the previous killed-only gate dropped that fact, making a reaped row indistinguishable from a natural failure. A row without the field still projects nothing.
|
|
24
|
+
- Brains: an ERROR tool result now discloses the media it had to drop (`[N images omitted — …]`, both wire formats) instead of silently presenting an attachment-free error; the vision-degrade predicates and placeholder wording collapse into one construction point (`media-degrade.ts`).
|
|
25
|
+
- Image read cap: the refusal for an oversized image without a downsampler now names `sharp` (declared in `optionalDependencies`) and the 64MB ceiling it unlocks — the cap itself is unchanged.
|
|
26
|
+
|
|
27
|
+
- Background-command spool (RB-235 close-out): a LIVE writer's spool file is append-only — the reclaim truncate now waits for the writer to be terminal, closing the stat→truncate race window that silently destroyed whole buffered write blocks (external audit reproduced 8192-multiple losses on 5.3.0; the fix's own red-first run recorded 65536/720896/294912-byte ledger gaps). Cost, stated honestly: a live writer past the hard cap no longer has physical space reclaimed until it dies (the reader stays bounded and byte-accounted as before); the file is reclaimed wholesale at writer terminal. The live-rotation disclosure marker is retired with the race (structurally impossible now).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `STALE_RUNNING_REAP_ATTRIBUTION` (exported): the reaper's settle wording reaches the package root, so an out-of-repo store twin can import the attribution instead of value-copying it against the contract kit (downstream pickup request).
|
|
32
|
+
- `TaskSpec.limits.brainCallGuardrailMs` + `RunnerDeps.brainCallGuardrailMs` (RB-458): an OUTER per-call guardrail on how long the engine waits for one brain call — default 1 hour, `false`/`0` switches it off, invalid values refuse loudly at prepare. Independent of brain cooperation: expiry aborts the brain's signal AND stops the engine's own waiting at every await point, so a custom Brain that ignores `options.signal` can no longer wedge a run forever (previously the de-facto bound was an accidental property of the bundled fetch stack). A run stopped this way terminates loud with `TaskResult.errorCode = "brain.call_guardrail_timeout"`; walltime-budgeted runs keep their own (usually tighter) clocks — the two race and the earlier one wins.
|
|
33
|
+
|
|
3
34
|
## 5.3.0 (2026-08-03)
|
|
4
35
|
|
|
5
36
|
_The A2A arc: client+server legs land end-to-end (all additive), the workflow-size notice lane closes its channel, and three naming/containment defects fix. Major stays locked at 5._
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from
|
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
7
7
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
8
|
+
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
9
|
import { ANTHROPIC_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
|
|
9
10
|
import { isThinkingLevel, reasoningBudgetShare, resolveEffort } from "./reasoning.js";
|
|
10
11
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
@@ -37,7 +38,7 @@ function toUserBlocks(content, supportsVision = true) {
|
|
|
37
38
|
blocks.push({ type: "image", source: { type: "base64", media_type: c.mimeType, data: c.data } });
|
|
38
39
|
else {
|
|
39
40
|
omitted++;
|
|
40
|
-
blocks.push({ type: "text", text:
|
|
41
|
+
blocks.push({ type: "text", text: IMAGE_OMITTED_NO_VISION });
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -46,7 +47,7 @@ function toUserBlocks(content, supportsVision = true) {
|
|
|
46
47
|
return blocks;
|
|
47
48
|
}
|
|
48
49
|
function toAnthropicMessages(ctx, model) {
|
|
49
|
-
const supportsVision = model
|
|
50
|
+
const supportsVision = modelSupportsVision(model);
|
|
50
51
|
const raw = [];
|
|
51
52
|
let pendingDocs = [];
|
|
52
53
|
const flushDocs = () => {
|
|
@@ -112,6 +113,15 @@ function toAnthropicMessages(ctx, model) {
|
|
|
112
113
|
inner.push({ type: "text", text: note });
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
if (m.isError) {
|
|
117
|
+
const note = errorResultMediaNote({ images: sendableImages(Array.isArray(rest) ? rest : []).length, documents: docs.length });
|
|
118
|
+
if (note !== undefined) {
|
|
119
|
+
if (typeof inner === "string")
|
|
120
|
+
inner = inner ? `${inner}\n${note}` : note;
|
|
121
|
+
else
|
|
122
|
+
inner.push({ type: "text", text: note });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
115
125
|
if (typeof inner === "string" ? inner.length === 0 : inner.length === 0)
|
|
116
126
|
inner = NO_CONTENT;
|
|
117
127
|
raw.push({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Model } from "../internal/llm.js";
|
|
2
|
+
export declare function modelSupportsVision(model?: Model): boolean;
|
|
3
|
+
export declare const IMAGE_OMITTED_NO_VISION = "[image omitted: the serving model does not support image input]";
|
|
4
|
+
export declare function imagesOmittedNoVisionNote(count: number): string;
|
|
5
|
+
export interface SendableImage {
|
|
6
|
+
type: "image";
|
|
7
|
+
data: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function sendableImages(blocks: readonly unknown[]): SendableImage[];
|
|
11
|
+
export declare function errorResultMediaNote(counts: {
|
|
12
|
+
images: number;
|
|
13
|
+
documents: number;
|
|
14
|
+
}): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function modelSupportsVision(model) {
|
|
2
|
+
return model?.input === undefined ? true : model.input.includes("image");
|
|
3
|
+
}
|
|
4
|
+
export const IMAGE_OMITTED_NO_VISION = "[image omitted: the serving model does not support image input]";
|
|
5
|
+
export function imagesOmittedNoVisionNote(count) {
|
|
6
|
+
return count > 1 ? `[${count} images omitted: the serving model does not support image input]` : IMAGE_OMITTED_NO_VISION;
|
|
7
|
+
}
|
|
8
|
+
export function sendableImages(blocks) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const b of blocks) {
|
|
11
|
+
const c = b;
|
|
12
|
+
if (c.type === "image" && typeof c.data === "string" && typeof c.mimeType === "string") {
|
|
13
|
+
out.push({ type: "image", data: c.data, mimeType: c.mimeType });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function plural(count, noun) {
|
|
19
|
+
return `${count} ${noun}${count > 1 ? "s" : ""}`;
|
|
20
|
+
}
|
|
21
|
+
export function errorResultMediaNote(counts) {
|
|
22
|
+
const dropped = [];
|
|
23
|
+
if (counts.images > 0)
|
|
24
|
+
dropped.push(plural(counts.images, "image"));
|
|
25
|
+
if (counts.documents > 0)
|
|
26
|
+
dropped.push(plural(counts.documents, "document"));
|
|
27
|
+
if (dropped.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
return `[${dropped.join(" and ")} omitted — an error tool result must be text-only; re-run the tool successfully if you need the attachment]`;
|
|
30
|
+
}
|
package/dist/brain/openai.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRepetitionPoll, createWalltimeGate, parseStreamedToolArgs } from
|
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
7
7
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
8
|
+
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
9
|
import { OPENAI_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
|
|
9
10
|
import { DEFAULT_EFFORT_LEVELS, isThinkingLevel, resolveEffort } from "./reasoning.js";
|
|
10
11
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
@@ -30,10 +31,6 @@ function textOf(content) {
|
|
|
30
31
|
.map((c) => c.text)
|
|
31
32
|
.join("");
|
|
32
33
|
}
|
|
33
|
-
function visionOf(model) {
|
|
34
|
-
return model?.input === undefined ? true : model.input.includes("image");
|
|
35
|
-
}
|
|
36
|
-
const IMAGE_OMITTED = "[image omitted: the serving model does not support image input]";
|
|
37
34
|
const NO_CONTENT = "(no content)";
|
|
38
35
|
const DOCUMENT_OMITTED = "[PDF document omitted: PDF direct read requires an Anthropic-family model; re-Read the file (the engine will extract text/render pages when possible) or extract the text via Bash instead (e.g. `pdftotext`).]";
|
|
39
36
|
function toUserContent(content, supportsVision = true) {
|
|
@@ -54,7 +51,7 @@ function toUserContent(content, supportsVision = true) {
|
|
|
54
51
|
const n = content.filter((c) => c.type === "image").length;
|
|
55
52
|
emitBrainTelemetry({ kind: "vision_placeholder", count: n });
|
|
56
53
|
const base = textOf(content);
|
|
57
|
-
const note = n
|
|
54
|
+
const note = imagesOmittedNoVisionNote(n);
|
|
58
55
|
const withNote = base ? `${base}\n${note}` : note;
|
|
59
56
|
return nDocs > 0 ? `${withNote}\n${DOCUMENT_OMITTED}` : withNote;
|
|
60
57
|
}
|
|
@@ -139,7 +136,7 @@ function applyThinking(body, model, reasoning) {
|
|
|
139
136
|
}
|
|
140
137
|
}
|
|
141
138
|
function toOpenAIMessages(ctx, replayThinking = true, model) {
|
|
142
|
-
const supportsVision =
|
|
139
|
+
const supportsVision = modelSupportsVision(model);
|
|
143
140
|
const out = [];
|
|
144
141
|
const needsEmptyReasoningField = !!model?.reasoning && thinkingCompat(model).requiresReasoningContentOnAssistantMessages === true;
|
|
145
142
|
if (ctx.systemPrompt) {
|
|
@@ -194,9 +191,8 @@ function toOpenAIMessages(ctx, replayThinking = true, model) {
|
|
|
194
191
|
out.push(msg);
|
|
195
192
|
}
|
|
196
193
|
else if (m.role === "toolResult") {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
: [];
|
|
194
|
+
const mediaImages = sendableImages(Array.isArray(m.content) ? m.content : []);
|
|
195
|
+
const rawImages = m.isError ? [] : mediaImages;
|
|
200
196
|
const images = supportsVision ? rawImages : [];
|
|
201
197
|
if (!supportsVision && rawImages.length > 0)
|
|
202
198
|
emitBrainTelemetry({ kind: "vision_placeholder", count: rawImages.length });
|
|
@@ -205,11 +201,13 @@ function toOpenAIMessages(ctx, replayThinking = true, model) {
|
|
|
205
201
|
emitBrainTelemetry({ kind: "document_placeholder", count: nDocs });
|
|
206
202
|
const baseText0 = textOf(m.content);
|
|
207
203
|
const baseText = nDocs > 0 ? (baseText0 ? `${baseText0}\n${DOCUMENT_OMITTED}` : DOCUMENT_OMITTED) : baseText0;
|
|
208
|
-
const text = !supportsVision && rawImages.length > 0 ? (baseText ? `${baseText}\n${
|
|
204
|
+
const text = !supportsVision && rawImages.length > 0 ? (baseText ? `${baseText}\n${IMAGE_OMITTED_NO_VISION}` : IMAGE_OMITTED_NO_VISION) : baseText;
|
|
205
|
+
const errorNote = m.isError ? errorResultMediaNote({ images: mediaImages.length, documents: 0 }) : undefined;
|
|
206
|
+
const finalText = errorNote === undefined ? text : text ? `${text}\n${errorNote}` : errorNote;
|
|
209
207
|
out.push({
|
|
210
208
|
role: "tool",
|
|
211
209
|
tool_call_id: m.toolCallId,
|
|
212
|
-
content: toolResultText(
|
|
210
|
+
content: toolResultText(finalText, images.length > 0, m.content),
|
|
213
211
|
});
|
|
214
212
|
for (const img of images)
|
|
215
213
|
pendingImages.push({ toolCallId: m.toolCallId, data: img.data, mimeType: img.mimeType });
|
package/dist/brain/timeout.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StreamFn } from "../internal/llm.js";
|
|
1
2
|
export interface BrainTimeoutConfig {
|
|
2
3
|
connectTimeoutMs?: number;
|
|
3
4
|
firstTokenTimeoutMs?: number;
|
|
@@ -10,3 +11,16 @@ export declare function createConnectController(connectTimeoutMs: number | undef
|
|
|
10
11
|
clearTimer: () => void;
|
|
11
12
|
dispose: () => void;
|
|
12
13
|
};
|
|
14
|
+
export declare const BRAIN_CALL_GUARDRAIL_DEFAULT_MS = 3600000;
|
|
15
|
+
export declare const BRAIN_CALL_GUARDRAIL_ERROR_CODE = "brain.call_guardrail_timeout";
|
|
16
|
+
export declare class BrainCallGuardrailTimeoutError extends Error {
|
|
17
|
+
readonly code = "brain.call_guardrail_timeout";
|
|
18
|
+
readonly limitMs: number;
|
|
19
|
+
constructor(limitMs: number);
|
|
20
|
+
}
|
|
21
|
+
export type BrainCallGuardrailKnob = number | false;
|
|
22
|
+
export declare function resolveBrainCallGuardrailMs(knob: BrainCallGuardrailKnob | undefined): number | undefined;
|
|
23
|
+
export interface BrainCallGuardrailRef {
|
|
24
|
+
timedOut?: BrainCallGuardrailTimeoutError;
|
|
25
|
+
}
|
|
26
|
+
export declare function withBrainCallGuardrail(streamFn: StreamFn, limitMs: number, ref: BrainCallGuardrailRef): StreamFn;
|
package/dist/brain/timeout.js
CHANGED
|
@@ -44,3 +44,117 @@ export function createConnectController(connectTimeoutMs, outerSignal, deadlineA
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
export const BRAIN_CALL_GUARDRAIL_DEFAULT_MS = 3_600_000;
|
|
48
|
+
export const BRAIN_CALL_GUARDRAIL_ERROR_CODE = "brain.call_guardrail_timeout";
|
|
49
|
+
export class BrainCallGuardrailTimeoutError extends Error {
|
|
50
|
+
code = BRAIN_CALL_GUARDRAIL_ERROR_CODE;
|
|
51
|
+
limitMs;
|
|
52
|
+
constructor(limitMs) {
|
|
53
|
+
super(`the engine stopped waiting for a brain call that ran past its ${limitMs}ms guardrail — the call was abandoned (a Brain that ignores options.signal may still be running)`);
|
|
54
|
+
this.name = "BrainCallGuardrailTimeoutError";
|
|
55
|
+
this.limitMs = limitMs;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function resolveBrainCallGuardrailMs(knob) {
|
|
59
|
+
if (knob === undefined)
|
|
60
|
+
return BRAIN_CALL_GUARDRAIL_DEFAULT_MS;
|
|
61
|
+
if (knob === false || knob === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (typeof knob !== "number" || !Number.isFinite(knob) || knob < 0) {
|
|
64
|
+
const e = new Error(`brainCallGuardrailMs must be a positive number of milliseconds, 0, or false (got ${String(knob)})`);
|
|
65
|
+
e.code = "config.brain_call_guardrail_invalid";
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
return knob;
|
|
69
|
+
}
|
|
70
|
+
function armCallGuardrail(limitMs, outerSignal, ref) {
|
|
71
|
+
const ac = new AbortController();
|
|
72
|
+
if (outerSignal !== undefined) {
|
|
73
|
+
if (outerSignal.aborted)
|
|
74
|
+
ac.abort(outerSignal.reason);
|
|
75
|
+
}
|
|
76
|
+
const forwardAbort = () => {
|
|
77
|
+
ac.abort(outerSignal === undefined ? undefined : outerSignal.reason);
|
|
78
|
+
};
|
|
79
|
+
if (outerSignal !== undefined && !outerSignal.aborted) {
|
|
80
|
+
outerSignal.addEventListener("abort", forwardAbort, { once: true });
|
|
81
|
+
}
|
|
82
|
+
let expired;
|
|
83
|
+
let rejectExpiry = () => undefined;
|
|
84
|
+
const expiry = new Promise((_resolve, reject) => {
|
|
85
|
+
rejectExpiry = reject;
|
|
86
|
+
});
|
|
87
|
+
expiry.catch(() => undefined);
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
const err = new BrainCallGuardrailTimeoutError(limitMs);
|
|
90
|
+
expired = err;
|
|
91
|
+
ref.timedOut ??= err;
|
|
92
|
+
ac.abort(err);
|
|
93
|
+
rejectExpiry(err);
|
|
94
|
+
}, limitMs);
|
|
95
|
+
return {
|
|
96
|
+
signal: ac.signal,
|
|
97
|
+
race: async (p) => {
|
|
98
|
+
if (expired !== undefined)
|
|
99
|
+
throw expired;
|
|
100
|
+
const value = await Promise.race([p, expiry]);
|
|
101
|
+
if (expired !== undefined)
|
|
102
|
+
throw expired;
|
|
103
|
+
return value;
|
|
104
|
+
},
|
|
105
|
+
disarm: () => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
if (outerSignal !== undefined)
|
|
108
|
+
outerSignal.removeEventListener("abort", forwardAbort);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function withBrainCallGuardrail(streamFn, limitMs, ref) {
|
|
113
|
+
return async (model, context, options) => {
|
|
114
|
+
const gate = armCallGuardrail(limitMs, options?.signal, ref);
|
|
115
|
+
let inner;
|
|
116
|
+
try {
|
|
117
|
+
inner = await gate.race(Promise.resolve(streamFn(model, context, { ...options, signal: gate.signal })));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
gate.disarm();
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
[Symbol.asyncIterator]() {
|
|
125
|
+
const it = inner[Symbol.asyncIterator]();
|
|
126
|
+
return {
|
|
127
|
+
next: async () => {
|
|
128
|
+
let step;
|
|
129
|
+
try {
|
|
130
|
+
step = await gate.race(Promise.resolve(it.next()));
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
gate.disarm();
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
if (step.done === true)
|
|
137
|
+
gate.disarm();
|
|
138
|
+
return step;
|
|
139
|
+
},
|
|
140
|
+
return: async () => {
|
|
141
|
+
gate.disarm();
|
|
142
|
+
const release = it.return;
|
|
143
|
+
if (release !== undefined) {
|
|
144
|
+
Promise.resolve(release.call(it)).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
return { value: undefined, done: true };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
result: async () => {
|
|
151
|
+
try {
|
|
152
|
+
return await gate.race(Promise.resolve(inner.result()));
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
gate.disarm();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
export type RedactionConfidence = "high" | "medium" | "low";
|
|
2
|
+
export type RedactionSource = "arg-summary" | "untrusted-egress" | "secret-env";
|
|
2
3
|
export interface RedactionFinding {
|
|
3
4
|
kind: string;
|
|
4
5
|
span: readonly [number, number];
|
|
5
6
|
confidence: RedactionConfidence;
|
|
6
7
|
marker: string;
|
|
8
|
+
source: RedactionSource;
|
|
7
9
|
}
|
|
10
|
+
export interface SummarizableFinding {
|
|
11
|
+
kind: string;
|
|
12
|
+
confidence: RedactionConfidence;
|
|
13
|
+
}
|
|
14
|
+
export declare function summarizeRedactions(findings: readonly SummarizableFinding[]): string;
|
|
8
15
|
export interface RedactionReport {
|
|
9
16
|
findings: RedactionFinding[];
|
|
10
17
|
preexistingMarkers?: number;
|
|
@@ -13,6 +20,7 @@ export interface RedactionPass {
|
|
|
13
20
|
kind: string;
|
|
14
21
|
confidence: RedactionConfidence;
|
|
15
22
|
marker: string;
|
|
23
|
+
source: RedactionSource;
|
|
16
24
|
re: RegExp;
|
|
17
25
|
replace: string | ((match: string, ...groups: string[]) => string);
|
|
18
26
|
}
|
package/dist/core/arg-summary.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
const ACTIVITY_ARG_MAX = 80;
|
|
2
2
|
const SENSITIVE_KEY = /token|secret|key|password|passwd|credential|auth/i;
|
|
3
|
+
export function summarizeRedactions(findings) {
|
|
4
|
+
if (findings.length === 0)
|
|
5
|
+
return "no redactions";
|
|
6
|
+
const counts = new Map();
|
|
7
|
+
for (const f of findings) {
|
|
8
|
+
const label = `${f.kind} (${f.confidence})`;
|
|
9
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
10
|
+
}
|
|
11
|
+
const parts = [...counts].map(([label, n]) => `${label} x${n}`);
|
|
12
|
+
return `${findings.length} redaction${findings.length === 1 ? "" : "s"}: ${parts.join(", ")}`;
|
|
13
|
+
}
|
|
3
14
|
function mapBackOnePass(edits, pos) {
|
|
4
15
|
let delta = 0;
|
|
5
16
|
for (const e of edits) {
|
|
@@ -37,7 +48,13 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
37
48
|
s0 = mapBackOnePass(batches[i], s0);
|
|
38
49
|
e0 = mapBackOnePass(batches[i], e0);
|
|
39
50
|
}
|
|
40
|
-
report.findings.push({
|
|
51
|
+
report.findings.push({
|
|
52
|
+
kind: pass.kind,
|
|
53
|
+
confidence: pass.confidence,
|
|
54
|
+
span: [s0, e0],
|
|
55
|
+
marker: pass.marker,
|
|
56
|
+
source: pass.source,
|
|
57
|
+
});
|
|
41
58
|
}
|
|
42
59
|
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length });
|
|
43
60
|
}
|
|
@@ -48,18 +65,19 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
48
65
|
return cur;
|
|
49
66
|
}
|
|
50
67
|
export const SECRET_PASSES = [
|
|
51
|
-
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
|
|
52
|
-
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
|
|
53
|
-
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
|
|
54
|
-
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
|
|
55
|
-
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
|
|
56
|
-
{ kind: "jwt", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
|
|
57
|
-
{ kind: "private-key-block", confidence: "high", marker: "[redacted]", re: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?(?:-----END[ A-Z]*PRIVATE KEY-----|$)/g, replace: "[redacted]" },
|
|
58
|
-
{ kind: "bearer-token", confidence: "high", marker: "[redacted]", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
|
|
68
|
+
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
|
|
69
|
+
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
|
|
70
|
+
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
|
|
71
|
+
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
|
|
72
|
+
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
|
|
73
|
+
{ kind: "jwt", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
|
|
74
|
+
{ kind: "private-key-block", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?(?:-----END[ A-Z]*PRIVATE KEY-----|$)/g, replace: "[redacted]" },
|
|
75
|
+
{ kind: "bearer-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
|
|
59
76
|
{
|
|
60
77
|
kind: "keyword-value",
|
|
61
78
|
confidence: "medium",
|
|
62
79
|
marker: "[redacted]",
|
|
80
|
+
source: "arg-summary",
|
|
63
81
|
re: /(?<![A-Za-z0-9])((?:token|secret|key|password|passwd|credential|authorization)["']?\s*[=:]\s*["']?)[^\s"';|&]{4,}/gi,
|
|
64
82
|
replace: "$1[redacted]",
|
|
65
83
|
},
|
|
@@ -30,4 +30,3 @@ export declare function sliceStreamIncrement(s: StreamCursorState, terminal: boo
|
|
|
30
30
|
droppedBeforeCursor: number;
|
|
31
31
|
};
|
|
32
32
|
export declare function markTruncated(text: string, droppedBytes: number): string;
|
|
33
|
-
export declare function markLiveSpoolRotations(text: string, liveRotations: number): string;
|
|
@@ -73,9 +73,3 @@ export function markTruncated(text, droppedBytes) {
|
|
|
73
73
|
? `[... ${droppedBytes} earlier byte(s) truncated; showing last ${Buffer.byteLength(text, "utf8")} ...]\n${text}`
|
|
74
74
|
: text;
|
|
75
75
|
}
|
|
76
|
-
export function markLiveSpoolRotations(text, liveRotations) {
|
|
77
|
-
if (liveRotations <= 0)
|
|
78
|
-
return text;
|
|
79
|
-
const note = `[spool reclaimed ${liveRotations} time(s) while the process was still writing; bytes appended during those windows may be missing beyond any counted truncation]`;
|
|
80
|
-
return text === "" ? note : `${text}${text.endsWith("\n") ? "" : "\n"}${note}`;
|
|
81
|
-
}
|
|
@@ -11,6 +11,7 @@ import { type ActiveSkillFrame } from "./active-skill-scope.js";
|
|
|
11
11
|
import { type CutKillRegistry } from "./cut-kill.js";
|
|
12
12
|
import type { SessionPermissionRules } from "../session-policy-store.js";
|
|
13
13
|
import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
|
|
14
|
+
import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
|
|
14
15
|
import { type OutputRef, type BlockedRef, type SkillListingEntry } from "./synthetic-tools.js";
|
|
15
16
|
import type { MemoryEngine } from "../memory-engine/engine.js";
|
|
16
17
|
import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
|
|
@@ -136,6 +137,7 @@ export interface Prepared {
|
|
|
136
137
|
callIssuedAtRef?: {
|
|
137
138
|
current?: number;
|
|
138
139
|
};
|
|
140
|
+
brainCallGuardrailRef: BrainCallGuardrailRef;
|
|
139
141
|
reviewRequestRef: {
|
|
140
142
|
pending?: {
|
|
141
143
|
reason?: string;
|
|
@@ -29,6 +29,7 @@ import { cloneObserverInput, formatHookFeedback, runToolGate } from "../hooks.js
|
|
|
29
29
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
30
30
|
import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
|
|
31
31
|
import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
|
|
32
|
+
import { resolveBrainCallGuardrailMs, withBrainCallGuardrail } from "../../brain/timeout.js";
|
|
32
33
|
import { defineTool, isDefineToolProduct } from "../tools.js";
|
|
33
34
|
import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
|
|
34
35
|
import { protocolOf, PROTOCOL_TABLE } from "../protocol-table.js";
|
|
@@ -236,6 +237,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
236
237
|
e.code = "config.empty_objective";
|
|
237
238
|
throw e;
|
|
238
239
|
}
|
|
240
|
+
const brainCallGuardrailRef = {};
|
|
241
|
+
const brainCallGuardrailMs = resolveBrainCallGuardrailMs(spec.limits?.brainCallGuardrailMs ?? deps.brainCallGuardrailMs);
|
|
239
242
|
if (spec.agents !== undefined && spec.agents.length > 0) {
|
|
240
243
|
const pool = spec.tools ?? [];
|
|
241
244
|
if (!pool.some((t) => typeof t.withAgents === "function")) {
|
|
@@ -1466,9 +1469,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1466
1469
|
if (spec.interactiveTools === true || (spec.interactiveTools !== false && (onQuestion !== undefined || durableQuestionFace)))
|
|
1467
1470
|
tools.push(createAskUserQuestionTool(onQuestion, { principal: spec.principal, sourceTaskId: sessionId }));
|
|
1468
1471
|
if (spec.handsReadOnly !== true) {
|
|
1469
|
-
const
|
|
1472
|
+
const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
|
|
1470
1473
|
tools.push(...createSchedulerTools(executionEnv, {
|
|
1471
|
-
scope:
|
|
1474
|
+
scope: sessionScope,
|
|
1472
1475
|
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
1473
1476
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1474
1477
|
requestStopAfterTurn,
|
|
@@ -2198,7 +2201,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2198
2201
|
systemPrompt,
|
|
2199
2202
|
...(assembled.systemBlocks ? { systemBlocks: assembled.systemBlocks } : {}),
|
|
2200
2203
|
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
2201
|
-
runtime: brainToRuntime(
|
|
2204
|
+
runtime: brainToRuntime(brainCallGuardrailMs === undefined
|
|
2205
|
+
? deps.brain
|
|
2206
|
+
: {
|
|
2207
|
+
...deps.brain,
|
|
2208
|
+
stream: withBrainCallGuardrail((m, c, o) => deps.brain.stream(m, c, o), brainCallGuardrailMs, brainCallGuardrailRef),
|
|
2209
|
+
}),
|
|
2202
2210
|
});
|
|
2203
2211
|
harnessRef.current = harness;
|
|
2204
2212
|
let releaseSignal = () => undefined;
|
|
@@ -3529,7 +3537,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3529
3537
|
: undefined;
|
|
3530
3538
|
overheadState.promptChars = systemPrompt.length;
|
|
3531
3539
|
const preparedHolder = {};
|
|
3532
|
-
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
3540
|
+
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
3533
3541
|
const prepared = buildPrepared();
|
|
3534
3542
|
preparedHolder.current = prepared;
|
|
3535
3543
|
return prepared;
|
|
@@ -2659,6 +2659,9 @@ export class Runner {
|
|
|
2659
2659
|
unsubBoundary();
|
|
2660
2660
|
unsub();
|
|
2661
2661
|
}
|
|
2662
|
+
if (threw === undefined && prepared.brainCallGuardrailRef.timedOut !== undefined) {
|
|
2663
|
+
threw = prepared.brainCallGuardrailRef.timedOut;
|
|
2664
|
+
}
|
|
2662
2665
|
const suspended = prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined;
|
|
2663
2666
|
const reviewPaused = prepared.reviewRef.token !== undefined && prepared.reviewRef.gate !== undefined;
|
|
2664
2667
|
const durablyPaused = suspended || reviewPaused;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { defineTool, errorResult } from "../tools.js";
|
|
4
|
+
import { formatZodValidationError, truncateError } from "../tool-errors.js";
|
|
4
5
|
export const TOOL_SEARCH_NAME = "ToolSearch";
|
|
5
6
|
const DEFER_AUTO_FRACTION = 0.1;
|
|
6
7
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -61,12 +62,32 @@ export function buildDeferredRegistry(deferred, tools) {
|
|
|
61
62
|
}
|
|
62
63
|
return reg;
|
|
63
64
|
}
|
|
65
|
+
function renderSchemaForModel(schema) {
|
|
66
|
+
let json;
|
|
67
|
+
try {
|
|
68
|
+
json = JSON.stringify(schema);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return json === undefined ? undefined : truncateError(json);
|
|
74
|
+
}
|
|
64
75
|
export function createPlaceholderTool(info, direct) {
|
|
65
76
|
const sn = safeName(info.name);
|
|
66
77
|
const teachingRejection = () => {
|
|
67
78
|
throw new Error(`Tool "${sn}" is not active yet. Call ${TOOL_SEARCH_NAME} with {"query":"select:${sn}"} ` +
|
|
68
79
|
`(or a keyword \`query\`) to load its full schema, then call ${sn} with the proper arguments.`);
|
|
69
80
|
};
|
|
81
|
+
const invalidArgumentsRejection = (target, params, schemaJson, ride) => {
|
|
82
|
+
const text = `Invalid arguments for \`${sn}\`: ${formatZodValidationError(target.parameters, params)} ` +
|
|
83
|
+
`\`${sn}\` is now active — its full parameter schema is below (and rides the next request). ` +
|
|
84
|
+
`Call \`${sn}\` again with arguments matching it.\nParameter schema: ${schemaJson}`;
|
|
85
|
+
return {
|
|
86
|
+
content: ride === undefined || ride === "" ? [{ type: "text", text }] : [{ type: "text", text }, { type: "text", text: ride }],
|
|
87
|
+
details: { invalidArguments: true },
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
70
91
|
if (direct !== undefined) {
|
|
71
92
|
return {
|
|
72
93
|
name: info.name,
|
|
@@ -78,14 +99,20 @@ export function createPlaceholderTool(info, direct) {
|
|
|
78
99
|
...(direct.executionMode !== undefined ? { executionMode: direct.executionMode } : {}),
|
|
79
100
|
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
80
101
|
const real = direct.resolveReal();
|
|
81
|
-
if (real
|
|
102
|
+
if (real === undefined)
|
|
103
|
+
return teachingRejection();
|
|
104
|
+
if (Value.Check(real.parameters, params)) {
|
|
82
105
|
const ride = await direct.activate();
|
|
83
106
|
const result = await real.invoke(toolCallId, params, signal, onUpdate);
|
|
84
107
|
if (ride === undefined || ride === "")
|
|
85
108
|
return result;
|
|
86
109
|
return { ...result, content: [...result.content, { type: "text", text: ride }] };
|
|
87
110
|
}
|
|
88
|
-
|
|
111
|
+
const schemaJson = renderSchemaForModel(real.parameters);
|
|
112
|
+
if (schemaJson === undefined)
|
|
113
|
+
return teachingRejection();
|
|
114
|
+
const ride = await direct.activate();
|
|
115
|
+
return invalidArgumentsRejection(real, params, schemaJson, ride);
|
|
89
116
|
},
|
|
90
117
|
};
|
|
91
118
|
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
import type { RedactionConfidence, RedactionSource } from "./arg-summary.js";
|
|
2
|
+
export type SecretEnvFindingKind = "suffix-rule" | "exact-name";
|
|
3
|
+
export interface SecretEnvFinding {
|
|
4
|
+
key: string;
|
|
5
|
+
kind: SecretEnvFindingKind;
|
|
6
|
+
confidence: RedactionConfidence;
|
|
7
|
+
source: RedactionSource;
|
|
8
|
+
}
|
|
1
9
|
export declare function isSecretEnvKey(key: string): boolean;
|
|
2
|
-
export declare function scrubSecretEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
10
|
+
export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
|
package/dist/core/secret-env.js
CHANGED
|
@@ -4,14 +4,31 @@ const SECRET_ENV_EXACT_NAMES = new Set([
|
|
|
4
4
|
"PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
|
|
5
5
|
"NPM_CONFIG__AUTH", "NPM_CONFIG__AUTHTOKEN",
|
|
6
6
|
]);
|
|
7
|
+
const SECRET_ENV_KIND_CONFIDENCE = {
|
|
8
|
+
"exact-name": "high",
|
|
9
|
+
"suffix-rule": "medium",
|
|
10
|
+
};
|
|
11
|
+
function classifySecretEnvKey(key) {
|
|
12
|
+
if (SECRET_ENV_RE.test(key))
|
|
13
|
+
return "suffix-rule";
|
|
14
|
+
if (SECRET_ENV_EXACT_NAMES.has(key.toUpperCase()))
|
|
15
|
+
return "exact-name";
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
7
18
|
export function isSecretEnvKey(key) {
|
|
8
|
-
return
|
|
19
|
+
return classifySecretEnvKey(key) !== undefined;
|
|
9
20
|
}
|
|
10
|
-
export function scrubSecretEnv(env) {
|
|
21
|
+
export function scrubSecretEnv(env, findings) {
|
|
11
22
|
const out = {};
|
|
12
23
|
for (const k of Object.keys(env)) {
|
|
13
|
-
|
|
24
|
+
const kind = classifySecretEnvKey(k);
|
|
25
|
+
if (kind === undefined) {
|
|
14
26
|
out[k] = env[k];
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (findings !== undefined) {
|
|
30
|
+
findings.push({ key: k, kind, confidence: SECRET_ENV_KIND_CONFIDENCE[kind], source: "secret-env" });
|
|
31
|
+
}
|
|
15
32
|
}
|
|
16
33
|
return out;
|
|
17
34
|
}
|
|
@@ -1016,7 +1016,7 @@ export function buildAgentPollDetails(input) {
|
|
|
1016
1016
|
status: input.status,
|
|
1017
1017
|
retrieval_status: input.retrievalStatus,
|
|
1018
1018
|
...(input.seq !== undefined ? { seq: input.seq } : {}),
|
|
1019
|
-
...(input.
|
|
1019
|
+
...(input.stoppedBy !== undefined ? { stoppedBy: input.stoppedBy } : {}),
|
|
1020
1020
|
...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
|
|
1021
1021
|
...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
|
|
1022
1022
|
...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
|
package/dist/core/types.d.ts
CHANGED
|
@@ -333,6 +333,7 @@ export interface TaskSpec {
|
|
|
333
333
|
};
|
|
334
334
|
callCapByDeadline?: false;
|
|
335
335
|
gracefulFinalize?: false;
|
|
336
|
+
brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
|
|
336
337
|
};
|
|
337
338
|
configOverrides?: ReadonlyArray<import("../config/catalog.js").ConfigOverrideDeclaration>;
|
|
338
339
|
forwardSubagentEvents?: boolean;
|
|
@@ -754,6 +755,7 @@ export interface RunnerDeps {
|
|
|
754
755
|
builtinWorkflows?: boolean;
|
|
755
756
|
onWorkflowAgentSpawn?: (handle: import("../orchestration/workflow.js").WorkflowAgentHandle) => void;
|
|
756
757
|
maxSuspends?: number;
|
|
758
|
+
brainCallGuardrailMs?: import("../brain/timeout.js").BrainCallGuardrailKnob;
|
|
757
759
|
now?: () => number;
|
|
758
760
|
memoryBackend?: import("./memory-engine/types.js").MemoryBackend;
|
|
759
761
|
memoryEngineDir?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RedactionReport } from "./arg-summary.js";
|
|
2
|
-
export type { RedactionFinding, RedactionReport, RedactionConfidence } from "./arg-summary.js";
|
|
2
|
+
export type { RedactionFinding, RedactionReport, RedactionConfidence, RedactionSource, SummarizableFinding, } from "./arg-summary.js";
|
|
3
|
+
export { summarizeRedactions } from "./arg-summary.js";
|
|
3
4
|
export declare function redactSecrets(s: string, report?: RedactionReport): string;
|
|
4
5
|
export declare function redactHostLeaks(s: string, report?: RedactionReport): string;
|
|
5
6
|
export declare function boundedRedactedSummary(value: unknown, max: number): string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
2
2
|
import { scrubSecrets, SECRET_PASSES, runRedactionPasses } from "./arg-summary.js";
|
|
3
3
|
import { sliceHeadSafe } from "./surrogate-safe-slice.js";
|
|
4
|
+
export { summarizeRedactions } from "./arg-summary.js";
|
|
4
5
|
const URI_SCHEME = String.raw `[A-Za-z][A-Za-z0-9+.\-_]{0,63}`;
|
|
5
6
|
const CREDENTIALS_IN_AUTHORITY = new RegExp(String.raw `(${URI_SCHEME}:\/\/)((?:[^/\s"'@]*@)+)`, "gi");
|
|
6
7
|
const URL_OF_ANY_PROTOCOL = new RegExp(String.raw `${URI_SCHEME}:\/\/[^\s"'\`]+`, "gi");
|
|
@@ -21,18 +22,19 @@ const SECRET_TIER_PASSES = [
|
|
|
21
22
|
kind: "url-credentials",
|
|
22
23
|
confidence: "medium",
|
|
23
24
|
marker: "[redacted-credentials]",
|
|
25
|
+
source: "untrusted-egress",
|
|
24
26
|
re: CREDENTIALS_IN_AUTHORITY,
|
|
25
27
|
replace: (_m, scheme, userinfo) => redactUserinfo(scheme, userinfo),
|
|
26
28
|
},
|
|
27
|
-
{ kind: "signed-url-param", confidence: "high", marker: "[redacted]", re: /([?&#](?:sig|signature|sas|code)=)[^&\s"'#]+/gi, replace: "$1[redacted]" },
|
|
29
|
+
{ kind: "signed-url-param", confidence: "high", marker: "[redacted]", source: "untrusted-egress", re: /([?&#](?:sig|signature|sas|code)=)[^&\s"'#]+/gi, replace: "$1[redacted]" },
|
|
28
30
|
];
|
|
29
31
|
export function redactSecrets(s, report) {
|
|
30
32
|
return runRedactionPasses(s, SECRET_TIER_PASSES, report);
|
|
31
33
|
}
|
|
32
34
|
const HOST_LEAK_PASSES = [
|
|
33
35
|
...SECRET_TIER_PASSES,
|
|
34
|
-
{ kind: "url", confidence: "medium", marker: "[redacted-url]", re: URL_OF_ANY_PROTOCOL, replace: "[redacted-url]" },
|
|
35
|
-
{ kind: "fs-path", confidence: "low", marker: "[redacted-path]", re: /(?:\/[A-Za-z0-9_.-]+){2,}/g, replace: "[redacted-path]" },
|
|
36
|
+
{ kind: "url", confidence: "medium", marker: "[redacted-url]", source: "untrusted-egress", re: URL_OF_ANY_PROTOCOL, replace: "[redacted-url]" },
|
|
37
|
+
{ kind: "fs-path", confidence: "low", marker: "[redacted-path]", source: "untrusted-egress", re: /(?:\/[A-Za-z0-9_.-]+){2,}/g, replace: "[redacted-path]" },
|
|
36
38
|
];
|
|
37
39
|
export function redactHostLeaks(s, report) {
|
|
38
40
|
return runRedactionPasses(s, HOST_LEAK_PASSES, report);
|
|
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline";
|
|
|
8
8
|
import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
|
|
9
9
|
import { killProcessTree, shutdownDebug } from "./kill-tree.js";
|
|
10
10
|
import { scrubSecretEnv } from "../../core/secret-env.js";
|
|
11
|
-
import { RollingTailBuffer,
|
|
11
|
+
import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
|
|
12
12
|
import { BackgroundShellError } from "../../core/background-shell.js";
|
|
13
13
|
import { SchedulerError } from "../../core/scheduler.js";
|
|
14
14
|
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
|
|
@@ -216,18 +216,14 @@ const EXEC_SPOOL_HARD_CAP_BYTES = 16 * 1024 * 1024;
|
|
|
216
216
|
export function decideSpoolReclaim(args) {
|
|
217
217
|
const rotate = args.rotateBytes ?? EXEC_SPOOL_ROTATE_BYTES;
|
|
218
218
|
const hardCap = args.hardCapBytes ?? EXEC_SPOOL_HARD_CAP_BYTES;
|
|
219
|
-
if (args.
|
|
219
|
+
if (args.writerLive)
|
|
220
220
|
return { reclaim: false };
|
|
221
|
-
if (args.
|
|
222
|
-
if (!args.writerLive)
|
|
223
|
-
return { reclaim: true, live: false, skippedBytes: 0 };
|
|
224
|
-
if (args.size >= hardCap)
|
|
225
|
-
return { reclaim: true, live: true, skippedBytes: 0 };
|
|
221
|
+
if (args.cursor < rotate)
|
|
226
222
|
return { reclaim: false };
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
if (args.size === args.cursor)
|
|
224
|
+
return { reclaim: true, skippedBytes: 0 };
|
|
225
|
+
if (args.size > args.cursor && args.size >= hardCap)
|
|
226
|
+
return { reclaim: true, skippedBytes: args.size - args.cursor };
|
|
231
227
|
return { reclaim: false };
|
|
232
228
|
}
|
|
233
229
|
const SIGNUM = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
|
|
@@ -330,8 +326,6 @@ export class NodeExecutionEnv {
|
|
|
330
326
|
const adoptOnTimeoutRef = {};
|
|
331
327
|
const pumpSpoolRef = {};
|
|
332
328
|
const drainSpoolRef = {};
|
|
333
|
-
const liveRotations = { out: 0, err: 0 };
|
|
334
|
-
const liveRotationsTotal = () => liveRotations.out + liveRotations.err;
|
|
335
329
|
const writerLive = () => !settled && child !== undefined && child.exitCode === null && child.signalCode === null;
|
|
336
330
|
const armForceSettle = (result) => {
|
|
337
331
|
if (forceSettleRef.current || settled)
|
|
@@ -351,7 +345,7 @@ export class NodeExecutionEnv {
|
|
|
351
345
|
drainSpoolRef.current?.();
|
|
352
346
|
const so = stdoutTail.result();
|
|
353
347
|
const se = stderrTail.result();
|
|
354
|
-
return { stdout: markTruncated(so.text, so.droppedBytes), stderr:
|
|
348
|
+
return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes) };
|
|
355
349
|
};
|
|
356
350
|
const onAbort = () => {
|
|
357
351
|
abortKillInFlight = true;
|
|
@@ -545,8 +539,6 @@ export class NodeExecutionEnv {
|
|
|
545
539
|
return;
|
|
546
540
|
if (d.skippedBytes > 0)
|
|
547
541
|
lane.tail.recordSkippedBytes(d.skippedBytes);
|
|
548
|
-
if (d.live)
|
|
549
|
-
liveRotations[lane.cursor]++;
|
|
550
542
|
truncateSync(lane.path, 0);
|
|
551
543
|
spoolCursor[lane.cursor] = 0;
|
|
552
544
|
}
|
|
@@ -583,9 +575,7 @@ export class NodeExecutionEnv {
|
|
|
583
575
|
if (settled || !child)
|
|
584
576
|
return false;
|
|
585
577
|
pumpSpoolRef.current?.();
|
|
586
|
-
const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined
|
|
587
|
-
? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err }, liveRotations: liveRotationsTotal() }
|
|
588
|
-
: undefined);
|
|
578
|
+
const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err } } : undefined);
|
|
589
579
|
if (shellId === undefined)
|
|
590
580
|
return false;
|
|
591
581
|
if (spoolPumpRef.current)
|
|
@@ -678,12 +668,12 @@ export class NodeExecutionEnv {
|
|
|
678
668
|
const signum = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
|
|
679
669
|
settle(ok({
|
|
680
670
|
stdout: markTruncated(so.text, so.droppedBytes),
|
|
681
|
-
stderr:
|
|
671
|
+
stderr: `${markTruncated(se.text, se.droppedBytes)}${se.text.endsWith("\n") || se.text === "" ? "" : "\n"}[process terminated by ${signal}]`,
|
|
682
672
|
exitCode: 128 + (signum[signal] ?? 9),
|
|
683
673
|
}));
|
|
684
674
|
return;
|
|
685
675
|
}
|
|
686
|
-
settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr:
|
|
676
|
+
settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes), exitCode: code ?? 0 }));
|
|
687
677
|
};
|
|
688
678
|
child.on("close", (code, signal) => settleFromTermination(code, signal));
|
|
689
679
|
child.on("exit", (code, signal) => {
|
|
@@ -1051,7 +1041,6 @@ export class NodeExecutionEnv {
|
|
|
1051
1041
|
...(handover !== undefined
|
|
1052
1042
|
? { spool: { outPath: handover.spool.outPath, errPath: handover.spool.errPath, fileCursor: { ...handover.cursors }, ephemeral: true } }
|
|
1053
1043
|
: {}),
|
|
1054
|
-
...(handover !== undefined && (handover.liveRotations ?? 0) > 0 ? { liveRotations: handover.liveRotations } : {}),
|
|
1055
1044
|
};
|
|
1056
1045
|
const seedOut = Buffer.from(stdoutSoFar.text, "utf8");
|
|
1057
1046
|
const seedErr = Buffer.from(stderrSoFar.text, "utf8");
|
|
@@ -1241,14 +1230,9 @@ export class NodeExecutionEnv {
|
|
|
1241
1230
|
const out = sliceStreamIncrement(entry.stdout, terminal);
|
|
1242
1231
|
const er = sliceStreamIncrement(entry.stderr, terminal);
|
|
1243
1232
|
const droppedBeforeCursor = out.droppedBeforeCursor + er.droppedBeforeCursor;
|
|
1244
|
-
let stderrInc = er.inc;
|
|
1245
|
-
if (terminal && (entry.liveRotations ?? 0) > 0 && entry.liveRotationsDisclosed !== true) {
|
|
1246
|
-
entry.liveRotationsDisclosed = true;
|
|
1247
|
-
stderrInc = markLiveSpoolRotations(er.inc, entry.liveRotations ?? 0);
|
|
1248
|
-
}
|
|
1249
1233
|
return ok({
|
|
1250
1234
|
stdout: out.inc,
|
|
1251
|
-
stderr:
|
|
1235
|
+
stderr: er.inc,
|
|
1252
1236
|
status: entry.status,
|
|
1253
1237
|
...(entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
|
|
1254
1238
|
...(droppedBeforeCursor > 0 ? { truncated: true, bytesDroppedBeforeCursor: droppedBeforeCursor } : {}),
|
|
@@ -1320,8 +1304,6 @@ export class NodeExecutionEnv {
|
|
|
1320
1304
|
stream.tail.recordSkippedBytes(d.skippedBytes);
|
|
1321
1305
|
stream.totalBytes += d.skippedBytes;
|
|
1322
1306
|
}
|
|
1323
|
-
if (d.live)
|
|
1324
|
-
entry.liveRotations = (entry.liveRotations ?? 0) + 1;
|
|
1325
1307
|
truncateSync(path, 0);
|
|
1326
1308
|
spool.fileCursor[lane] = 0;
|
|
1327
1309
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
|
|
|
57
57
|
export type { KillProcessTreeOptions } from "./engine/execution-env/kill-tree.js";
|
|
58
58
|
export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
|
|
59
59
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
60
|
+
export type { SecretEnvFinding, SecretEnvFindingKind } from "./core/secret-env.js";
|
|
60
61
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
61
62
|
export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode } from "./internal/harness.js";
|
|
62
63
|
export type { ExecResult } from "./internal/harness.js";
|
|
@@ -165,14 +166,15 @@ export { toolResultStoreContract } from "./core/store-contracts/tool-result-stor
|
|
|
165
166
|
export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
|
|
166
167
|
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
167
168
|
export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
|
|
168
|
-
export { type BackgroundAgentStore, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentUsage, canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, type BackgroundAgentQuery, } from "./core/background-agent-store.js";
|
|
169
|
+
export { type BackgroundAgentStore, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentUsage, canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, type BackgroundAgentQuery, } from "./core/background-agent-store.js";
|
|
169
170
|
export { serveDurableAgentRowLane, buildAgentPollDetails, type AgentPollDetailsInput, } from "./core/task-registry-agent.js";
|
|
170
171
|
export type { UnifiedTaskResult } from "./core/task-registry-shared.js";
|
|
171
172
|
export { FileBackgroundAgentStore, type FileBackgroundAgentStoreOptions } from "./stores/file/background-agent-store.js";
|
|
172
173
|
export { A2A_TASK_STATES, type A2ATaskState, type A2ATaskStateReversal, type A2ATaskStateReversalFaithful, type A2ATaskStateReversalLossy, toA2ATaskState, fromA2ATaskState, } from "./core/a2a-task-state.js";
|
|
173
174
|
export { type WorkflowJournalStore, type WorkflowJournalEntry, InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
|
|
174
175
|
export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedactedSummary } from "./core/untrusted-egress.js";
|
|
175
|
-
export type { RedactionFinding, RedactionReport, RedactionConfidence } from "./core/untrusted-egress.js";
|
|
176
|
+
export type { RedactionFinding, RedactionReport, RedactionConfidence, RedactionSource, SummarizableFinding, } from "./core/untrusted-egress.js";
|
|
177
|
+
export { summarizeRedactions } from "./core/untrusted-egress.js";
|
|
176
178
|
export { MemoryRosterStore, FileRosterStore, type RosterStore, type RosterEntry, type RosterAccess, type RosterGcOptions } from "./agents/roster-store.js";
|
|
177
179
|
export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, type ConfigKnob, type ConfigOverrideDeclaration, type ConfigProvenance, type EffectiveConfigField, } from "./config/catalog.js";
|
|
178
180
|
export { normalizeAgentName } from "./core/task-registry.js";
|
package/dist/index.js
CHANGED
|
@@ -151,12 +151,13 @@ export { toolResultStoreContract } from "./core/store-contracts/tool-result-stor
|
|
|
151
151
|
export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
|
|
152
152
|
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
153
153
|
export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
|
|
154
|
-
export { canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, } from "./core/background-agent-store.js";
|
|
154
|
+
export { canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, } from "./core/background-agent-store.js";
|
|
155
155
|
export { serveDurableAgentRowLane, buildAgentPollDetails, } from "./core/task-registry-agent.js";
|
|
156
156
|
export { FileBackgroundAgentStore } from "./stores/file/background-agent-store.js";
|
|
157
157
|
export { A2A_TASK_STATES, toA2ATaskState, fromA2ATaskState, } from "./core/a2a-task-state.js";
|
|
158
158
|
export { InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
|
|
159
159
|
export { untrustedEgressForHuman, redactHostLeaks, redactSecrets, boundedRedactedSummary } from "./core/untrusted-egress.js";
|
|
160
|
+
export { summarizeRedactions } from "./core/untrusted-egress.js";
|
|
160
161
|
export { MemoryRosterStore, FileRosterStore } from "./agents/roster-store.js";
|
|
161
162
|
export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, } from "./config/catalog.js";
|
|
162
163
|
export { normalizeAgentName } from "./core/task-registry.js";
|
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -5,7 +5,7 @@ import { decodeTextBytes } from "./encoding.js";
|
|
|
5
5
|
import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
|
|
6
6
|
import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
|
|
7
7
|
import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
|
|
8
|
-
import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, READ_CYBER_REMINDER, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
|
|
8
|
+
import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, READ_CYBER_REMINDER, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
|
|
9
9
|
import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
|
|
10
10
|
export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption) {
|
|
11
11
|
return defineTool({
|
|
@@ -70,14 +70,16 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
70
70
|
}
|
|
71
71
|
const downsampler = imageDownsampler === false ? undefined : (imageDownsampler ?? (await resolveAutoDownsampler()));
|
|
72
72
|
const readCap = downsampler ? MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES : MAX_IMAGE_READ_BYTES;
|
|
73
|
+
const tooLargeToRead = (bytes) => `Error (Read): image "${path}" is too large to read (${bytes} bytes > ${readCap}-byte cap).` +
|
|
74
|
+
(downsampler ? "" : ` ${NO_DOWNSAMPLER_IMAGE_CAP_HINT}`);
|
|
73
75
|
if (meta.value.size > readCap) {
|
|
74
|
-
return errorResult(
|
|
76
|
+
return errorResult(tooLargeToRead(meta.value.size));
|
|
75
77
|
}
|
|
76
78
|
const bin = await env.readBinaryFile(r.key, ctx.signal);
|
|
77
79
|
if (!bin.ok)
|
|
78
80
|
return errorResult(`Error (Read): cannot read image "${path}": ${bin.error.message}`);
|
|
79
81
|
if (bin.value.byteLength > readCap) {
|
|
80
|
-
return errorResult(
|
|
82
|
+
return errorResult(tooLargeToRead(bin.value.byteLength));
|
|
81
83
|
}
|
|
82
84
|
if (!imageMagicMatches(bin.value, imageMime)) {
|
|
83
85
|
return errorResult(`Error (Read): "${path}" has an image extension but its content is not a valid ${imageMime} (empty, truncated, or mis-named).`);
|
|
@@ -18,6 +18,7 @@ export declare function persistedTextOf(encoded: string | Uint8Array): string;
|
|
|
18
18
|
export declare function notReadRefusalText(env: ExecutionEnv, toolName: string, key: string, v: Pick<FsViolation, "code" | "message" | "partialView">, signal?: AbortSignal, fallbackHint?: string): Promise<string>;
|
|
19
19
|
export declare const MAX_IMAGE_READ_BYTES: number;
|
|
20
20
|
export declare const MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES: number;
|
|
21
|
+
export declare const NO_DOWNSAMPLER_IMAGE_CAP_HINT = "Install the optional \"sharp\" dependency (declared in this package's optionalDependencies) to read images up to 67108864 bytes \u2014 it downsamples them to fit the API image limit.";
|
|
21
22
|
export type ReadImageDownsamplerOption = ImageDownsampler | false | undefined;
|
|
22
23
|
export declare function resolveAutoDownsampler(): Promise<ImageDownsampler | undefined>;
|
|
23
24
|
export declare const MAX_READ_OUTPUT_CHARS = 100000;
|
|
@@ -41,6 +41,7 @@ export async function notReadRefusalText(env, toolName, key, v, signal, fallback
|
|
|
41
41
|
}
|
|
42
42
|
export const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024;
|
|
43
43
|
export const MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES = 64 * 1024 * 1024;
|
|
44
|
+
export const NO_DOWNSAMPLER_IMAGE_CAP_HINT = `Install the optional "sharp" dependency (declared in this package's optionalDependencies) to read images up to ${MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES} bytes — it downsamples them to fit the API image limit.`;
|
|
44
45
|
let autoDownsampler;
|
|
45
46
|
export function resolveAutoDownsampler() {
|
|
46
47
|
return (autoDownsampler ??= sharpImageDownsampler());
|
|
@@ -27,9 +27,12 @@ The runtime clamps to [60, 3600], so you don't need to clamp yourself.
|
|
|
27
27
|
|
|
28
28
|
One short sentence on what you chose and why. Goes to telemetry and is shown back to the user. "watching CI run" beats "waiting." The user reads this to understand what you're doing without having to predict your cadence in advance — make it specific.
|
|
29
29
|
`;
|
|
30
|
-
function
|
|
30
|
+
function durableScopeOf(c) {
|
|
31
|
+
return c.principal ?? "default";
|
|
32
|
+
}
|
|
33
|
+
function toSchedulerContext(c, scope) {
|
|
31
34
|
return {
|
|
32
|
-
scope
|
|
35
|
+
scope,
|
|
33
36
|
...(c.principal !== undefined ? { principal: c.principal } : {}),
|
|
34
37
|
...(c.sessionId !== undefined ? { sessionId: c.sessionId } : {}),
|
|
35
38
|
...(c.taskConfig !== undefined ? { taskConfig: c.taskConfig } : {}),
|
|
@@ -157,7 +160,9 @@ export function createSchedulerTools(env, ctx) {
|
|
|
157
160
|
if (!hasScheduler(env))
|
|
158
161
|
return [];
|
|
159
162
|
const sched = env;
|
|
160
|
-
const
|
|
163
|
+
const sessionCtx = toSchedulerContext(ctx, ctx.scope);
|
|
164
|
+
const durableCtx = toSchedulerContext(ctx, durableScopeOf(ctx));
|
|
165
|
+
const lanesDiffer = sessionCtx.scope !== durableCtx.scope;
|
|
161
166
|
let cronCreateChain = Promise.resolve();
|
|
162
167
|
const serializedCronCreateOp = (fn) => {
|
|
163
168
|
const next = cronCreateChain.then(fn, fn);
|
|
@@ -255,9 +260,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
255
260
|
...(!durable ? { lifetime: "session" } : {}),
|
|
256
261
|
...(a.recurring !== undefined ? { recurring: a.recurring } : {}),
|
|
257
262
|
};
|
|
258
|
-
const
|
|
263
|
+
const opCtx = durable ? durableCtx : sessionCtx;
|
|
264
|
+
const before = await sched.list(opCtx);
|
|
259
265
|
const knownIds = before.ok ? new Set(before.value.map((s) => s.id)) : undefined;
|
|
260
|
-
const r = await sched.schedule(intent,
|
|
266
|
+
const r = await sched.schedule(intent, opCtx);
|
|
261
267
|
if (!r.ok)
|
|
262
268
|
return errorResult(`Error (CronCreate): ${r.error.message}`);
|
|
263
269
|
const replaced = knownIds?.has(r.value.id);
|
|
@@ -285,6 +291,16 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
285
291
|
};
|
|
286
292
|
}),
|
|
287
293
|
});
|
|
294
|
+
const cancelLanesFor = async (id) => {
|
|
295
|
+
if (!lanesDiffer)
|
|
296
|
+
return [sessionCtx];
|
|
297
|
+
for (const lane of [sessionCtx, durableCtx]) {
|
|
298
|
+
const listed = await sched.list(lane);
|
|
299
|
+
if (listed.ok && listed.value.some((s) => s.id === id))
|
|
300
|
+
return [lane];
|
|
301
|
+
}
|
|
302
|
+
return [sessionCtx, durableCtx];
|
|
303
|
+
};
|
|
288
304
|
const cronCancel = defineTool({
|
|
289
305
|
name: "CronDelete",
|
|
290
306
|
contract: { contractId: "core.cron_delete@1", implementationRevision: "1" },
|
|
@@ -293,7 +309,14 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
293
309
|
effect: "idempotent",
|
|
294
310
|
execute: async (args) => {
|
|
295
311
|
const { id } = args;
|
|
296
|
-
const
|
|
312
|
+
const taskId = id;
|
|
313
|
+
const [firstLane, ...otherLanes] = await cancelLanesFor(taskId);
|
|
314
|
+
let r = await sched.cancel(taskId, firstLane);
|
|
315
|
+
for (const lane of otherLanes) {
|
|
316
|
+
if (r.ok || r.error.code !== "not_found")
|
|
317
|
+
break;
|
|
318
|
+
r = await sched.cancel(taskId, lane);
|
|
319
|
+
}
|
|
297
320
|
if (!r.ok) {
|
|
298
321
|
if (r.error.code === "not_found")
|
|
299
322
|
return `CronDelete: no scheduled task "${id}" (already gone or not yours).`;
|
|
@@ -309,13 +332,23 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
309
332
|
parameters: Type.Object({}),
|
|
310
333
|
effect: "read",
|
|
311
334
|
execute: async () => {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
335
|
+
const rows = [];
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
for (const lane of lanesDiffer ? [sessionCtx, durableCtx] : [sessionCtx]) {
|
|
338
|
+
const listed = await sched.list(lane);
|
|
339
|
+
if (!listed.ok)
|
|
340
|
+
return errorResult(`Error (CronList): ${listed.error.message}`);
|
|
341
|
+
for (const s of listed.value) {
|
|
342
|
+
if (seen.has(s.id))
|
|
343
|
+
continue;
|
|
344
|
+
seen.add(s.id);
|
|
345
|
+
rows.push(s);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (rows.length === 0) {
|
|
316
349
|
return { content: "No scheduled tasks.", details: { type: "cron-list", jobs: [] } };
|
|
317
350
|
}
|
|
318
|
-
const content =
|
|
351
|
+
const content = rows
|
|
319
352
|
.map((s) => {
|
|
320
353
|
const shown = cronExprFromSummary(s.when);
|
|
321
354
|
const human = cronToHuman(s.when);
|
|
@@ -324,7 +357,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
324
357
|
return `- ${s.id}: ${shown}${human !== s.when ? ` — ${human}` : ""}${s.label ? ` (${s.label})` : ""}${tier}${once}`;
|
|
325
358
|
})
|
|
326
359
|
.join("\n");
|
|
327
|
-
const jobs =
|
|
360
|
+
const jobs = rows.map((s) => {
|
|
328
361
|
const human = cronToHuman(s.when);
|
|
329
362
|
return {
|
|
330
363
|
id: s.id,
|
|
@@ -339,7 +372,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
339
372
|
},
|
|
340
373
|
});
|
|
341
374
|
const listPendingWakeups = async () => {
|
|
342
|
-
const r = await sched.list(
|
|
375
|
+
const r = await sched.list(sessionCtx);
|
|
343
376
|
if (!r.ok)
|
|
344
377
|
return { err: r.error.message };
|
|
345
378
|
return {
|
|
@@ -351,7 +384,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
351
384
|
const cancelWakeups = async (ids) => {
|
|
352
385
|
let n = 0;
|
|
353
386
|
for (const id of ids) {
|
|
354
|
-
const r = await sched.cancel(id,
|
|
387
|
+
const r = await sched.cancel(id, sessionCtx);
|
|
355
388
|
if (r.ok)
|
|
356
389
|
n += 1;
|
|
357
390
|
}
|
|
@@ -405,7 +438,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
405
438
|
const pending = await listPendingWakeups();
|
|
406
439
|
if (!("err" in pending))
|
|
407
440
|
await cancelWakeups(pending.ids);
|
|
408
|
-
const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" },
|
|
441
|
+
const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" }, sessionCtx);
|
|
409
442
|
if (!r.ok)
|
|
410
443
|
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`);
|
|
411
444
|
const scheduledFor = Date.now() + clampedDelaySeconds * 1000;
|
package/dist/tools/web.js
CHANGED
|
@@ -211,11 +211,14 @@ async function fetchAllowlisted(doFetch, start, allowHosts, userAgent, signal, o
|
|
|
211
211
|
export function webFetchToolSpec(config = {}) {
|
|
212
212
|
const maxBytes = config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
213
213
|
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
214
|
+
const canSummarize = config.summarize !== undefined;
|
|
214
215
|
return {
|
|
215
216
|
name: "WebFetch",
|
|
216
217
|
contract: { contractId: "core.web_fetch@1", implementationRevision: "1" },
|
|
217
|
-
description:
|
|
218
|
-
"
|
|
218
|
+
description: (canSummarize
|
|
219
|
+
? "Fetches a URL, converts the page to markdown, and answers `prompt` against it with a sub-model.\n"
|
|
220
|
+
: "Fetches a URL, converts the page to markdown, and returns the markdown. This deployment has no " +
|
|
221
|
+
"extraction sub-model wired, so pages come back in full for you to read.\n") +
|
|
219
222
|
"\n" +
|
|
220
223
|
"- Fails on authenticated/private URLs — use an authenticated MCP tool or `gh` for those instead.\n" +
|
|
221
224
|
"- Cross-host redirects are returned to you rather than followed; call again with the redirect URL.\n" +
|
|
@@ -223,7 +226,9 @@ export function webFetchToolSpec(config = {}) {
|
|
|
223
226
|
"hosts are always refused; if an allowlist is configured, only those hosts are reachable.",
|
|
224
227
|
parameters: Type.Object({
|
|
225
228
|
url: Type.String({ description: "The URL to fetch content from" }),
|
|
226
|
-
|
|
229
|
+
...(canSummarize
|
|
230
|
+
? { prompt: Type.Optional(Type.String({ description: "The prompt to run on the fetched content (answered by the configured sub-model)." })) }
|
|
231
|
+
: {}),
|
|
227
232
|
}),
|
|
228
233
|
effect: "read",
|
|
229
234
|
execute: async (args, ctx) => {
|
|
@@ -434,9 +439,12 @@ export function webFetchToolSpec(config = {}) {
|
|
|
434
439
|
: bodyCut
|
|
435
440
|
? `\n\n[WebFetch: ${cutPhrase} — the error body itself was cut off mid-transfer: ${bodyBytes?.length ?? 0} bytes were received before the cutoff and the tail is missing]`
|
|
436
441
|
: "";
|
|
442
|
+
const promptNote = prompt
|
|
443
|
+
? `\n\n[note: the requested analysis ("${inlineUntrusted(prompt, 120)}") was NOT applied — the fetch failed with HTTP ${res.status}, so there is no page content to run it against; anything below is the server's error response, not an answer]`
|
|
444
|
+
: "";
|
|
437
445
|
const content = (excerpt
|
|
438
446
|
? `${headline}\nThe error response body${trimmed.length > excerpt.length ? ` (first ${ERROR_BODY_EXCERPT_CHARS} chars)` : ""} follows:\n\n${delimitUntrusted(`WebFetch ${parsed.hostname}`, excerpt)}`
|
|
439
|
-
: headline) + bodyStateNote;
|
|
447
|
+
: headline) + bodyStateNote + promptNote;
|
|
440
448
|
return {
|
|
441
449
|
content,
|
|
442
450
|
details: {
|