@sema-agent/core 5.5.0 → 5.7.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 +32 -0
- package/dist/brain/stream-engine.js +4 -4
- package/dist/brain/timeout.d.ts +1 -0
- package/dist/brain/timeout.js +10 -0
- package/dist/core/checkpoint-store.d.ts +5 -2
- package/dist/core/checkpoint-store.js +6 -3
- package/dist/core/runner/runtask.js +67 -9
- package/dist/core/secret-env.d.ts +2 -0
- package/dist/core/secret-env.js +16 -4
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-notification.js +3 -0
- package/dist/tools/fs/fs-read.js +6 -1
- package/dist/tools/fs/fs-write.js +4 -4
- package/dist/tools/fs/safety.d.ts +11 -1
- package/dist/tools/fs/safety.js +84 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.7.0 (2026-08-04)
|
|
4
|
+
|
|
5
|
+
_Three collected cars: a parked question is answered by the decision itself (behavior face — see the consumer note), a descendant's terminal notification anchors on the delegation tree, and the env scrub stops deleting infrastructure key names (behavior face)._
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A descendant background agent's terminal notification now anchors on the delegation tree instead of dying with its spawner. A background agent notifies through the injector of the run that spawned it, and that injector parked on that run's OWN session once its lane was gone — so a grandchild launched from a delegated run (root → child A → background grandchild B; A completes, B settles later) parked its terminal on a throwaway child session that never runs again, and the root conversation was never told (the data was never lost — `TaskOutput` still answered; the gap was purely the push face). A delegated agent's TERMINAL frame now also escalates one hop up the delegation chain: a live ancestor takes it in its current run, a dead one repeats the decision one level higher, and the walk ends at the tree root; without a chain link the recorded root anchor is used directly. Strictly additive: the own-session park is kept (a retained child session later resumed still receives what it always received), the live lane is untouched, and every other notification class (shell completions, SendMessage deliveries, external `notify()` events) stays addressed to its own session. A delegated run that can reach neither an uplink nor a root anchor still parks (fail-open) and discloses once (`degraded` / `descendant-terminal-unrouted`).
|
|
10
|
+
|
|
11
|
+
- The env scrub no longer deletes infrastructure key names. `SECRET_ENV_RE`'s bare `KEY` word matched `PARTITION_KEY` / `SORT_KEY` / `RANGE_KEY` / `IDEMPOTENCY_KEY` / `CACHE_KEY` / `ROUTING_KEY`, and `scrubSecretEnv` DELETES what it matches — a spawned child silently lost variables that carry no credential (writes to a wrong partition, a routing key nothing binds). The word itself cannot be withdrawn (`API_KEY` / `SECRET_KEY` / `PRIVATE_KEY` / `ACCESS_KEY` are the bulk of real credential names), so the fix is the mirror of the existing positive exact-name list: `NON_SECRET_ENV_EXACT_NAMES`, exact names (never a shape — a pattern exemption would be a bypass) confirmed non-credential, consulted before the shape rule with the credential list read first (a name on both lists resolves to the credential reading). An exempted entry is kept and mints no finding. Decorated variants (`USERS_SORT_KEY`, `SORT_KEY_2`) are still dropped. **Consumers pinning the old shape**: a probe asserting `isSecretEnvKey("SORT_KEY") === true`, or that a scrub drops one of the six exempt names, now reads the opposite answer.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A durable question can be answered by the decision itself. `ResumeOutcome`'s `policy_ask` arm takes an optional `answer` (a `QuestionAnswer`) when the checkpoint's pending call is the reserved `AskUserQuestion` tool; the resume binds it as the resumed leg's answering face, so the pending call executes against the operator's actual selections. This is the same wiring the documented live arm builds by hand (`resume(token, outcome, { ...config, onQuestion: async () => answer })`) — but minted from the payload, which is the only route available to a caller redeeming an **offline background child**: the revive drive rebuilds that leg's config from the parked row and has no seat to put a closure in, so approving a parked question used to feed the model a fabricated "no human is available" default. The answer is untrusted wire data throughout: it goes through the existing answer fence unchanged (`selected ⊆ options` for the trusted line, off-list values and free-text `note` in a data fence), so multi-select and "Other" free text behave identically on both arms. The binding is one-shot — it answers the decided call only; a new question on the resumed leg is not fed the old answer. The answer is also recorded on the persisted winner, so an `env_failed` reopen must replay the identical answer (a replay may not keep the verdict and swap the text).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Behavior face** — approving a durable question without an answer is now refused instead of consumed. A `policy_ask` `allow` whose pending call is the reserved question tool is rejected **pre-CAS** (`checkpoint.invalid_outcome`, `detail.field: "answer"`, the checkpoint stays `pending` and redeemable) when it carries neither an `answer` nor a live answering face — where "live" excludes the reserved `QUESTION_AWAITS_RESUME` placeholder, whose contract is that it must never run. Previously that shape consumed the human's approval and let the run finish on a fabricated default (or, with the placeholder, on its config error) with nothing left to redeem. A resume that supplies its own real `onQuestion` is unaffected; so is every non-question approval. Symmetrically, an `answer` attached to a decision that cannot carry one (a `deny`, or a pending call that is not the question tool) is rejected the same way rather than silently dropped. **Consumers pinning the old shape**: a probe that approved a parked question with a bare allow now gets a typed rejection — supply the answer on the outcome, or deny.
|
|
20
|
+
|
|
21
|
+
## 5.6.0 (2026-08-03)
|
|
22
|
+
|
|
23
|
+
_A hardening tail: numeric knobs refuse what they cannot honor, the binary gate reads bytes instead of trusting statistics, and the MCP SDK takes its last zero-cost step before the v2 jump._
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Stall-timeout knobs (`connectTimeoutMs` / `firstTokenTimeoutMs` / `idleTimeoutMs`) refuse invalid numbers loudly (`config.stall_timeout_invalid`) instead of silently disarming — a NaN (units fumble: `"120s"` → `Number` → NaN) used to read as "unset", switching every watchdog OFF exactly when a deployment meant to tighten them. `0` stays legal as an explicit off; absent stays absent.
|
|
28
|
+
|
|
29
|
+
- Read's binary-extension list gains magic-byte criteria of its own (~40 formats, one data-driven signature table): a file whose bytes carry a real binary header (ZIP/gzip/7z/ELF/tar/RIFF…) is now refused by CONTENT even when printable padding used to slip it past the statistical sniffer as mojibake "text". Names still never decide: a `.zip` holding plain text still reads. The Edit/Write/NotebookEdit read-refusal texts all name the same escape route (one constant; NotebookEdit used to render the bare sentence with no hint at all).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `@modelcontextprotocol/sdk` 1.29.0 → 1.30.0 (exact pin). Public surface is purely additive upstream (stdio `maxBufferSize`, SSE `keepAliveMs`); `types` is byte-identical; the only behavior delta is a richer zod parse-error message the engine never pins on. Groundwork for the v2 three-package migration (spiked, GO, waiting out the first patch release).
|
|
34
|
+
|
|
3
35
|
## 5.5.0 (2026-08-03)
|
|
4
36
|
|
|
5
37
|
_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._
|
|
@@ -2,7 +2,7 @@ import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
|
2
2
|
import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
|
|
3
3
|
import { retryBackoffMs } from "./retry.js";
|
|
4
4
|
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
5
|
-
import { createConnectController } from "./timeout.js";
|
|
5
|
+
import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
|
|
6
6
|
import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
|
|
7
7
|
const DEFAULT_MAX_RETRIES = 10;
|
|
8
8
|
const MAX_RETRIES_ENV_CEILING = 15;
|
|
@@ -113,9 +113,9 @@ export function runStreamingBrain(args) {
|
|
|
113
113
|
const req = buildRequest();
|
|
114
114
|
const maxRetries = resolveMaxRetries(config.maxRetries);
|
|
115
115
|
const baseDelay = config.retryDelayMs ?? 500;
|
|
116
|
-
const firstTokenTimeoutMs = config.firstTokenTimeoutMs ?? stallTimeouts?.firstTokenMs;
|
|
117
|
-
const idleTimeoutMs = config.idleTimeoutMs ?? stallTimeouts?.idleMs;
|
|
118
|
-
const connectTimeoutMs = config.connectTimeoutMs ?? stallTimeouts?.connectMs;
|
|
116
|
+
const firstTokenTimeoutMs = resolveStallTimeoutMs(config.firstTokenTimeoutMs ?? stallTimeouts?.firstTokenMs, "firstTokenTimeoutMs");
|
|
117
|
+
const idleTimeoutMs = resolveStallTimeoutMs(config.idleTimeoutMs ?? stallTimeouts?.idleMs, "idleTimeoutMs");
|
|
118
|
+
const connectTimeoutMs = resolveStallTimeoutMs(config.connectTimeoutMs ?? stallTimeouts?.connectMs, "connectTimeoutMs");
|
|
119
119
|
let attempt = 0;
|
|
120
120
|
let thinkingRetries = 0;
|
|
121
121
|
let startEmitted = false;
|
package/dist/brain/timeout.d.ts
CHANGED
package/dist/brain/timeout.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export function resolveStallTimeoutMs(value, knob) {
|
|
2
|
+
if (value === undefined)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
5
|
+
const e = new Error(`${knob} must be a non-negative finite number of milliseconds (got ${String(value)})`);
|
|
6
|
+
e.code = "config.stall_timeout_invalid";
|
|
7
|
+
throw e;
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
1
11
|
export function createConnectController(connectTimeoutMs, outerSignal, deadlineAtMs) {
|
|
2
12
|
const ac = new AbortController();
|
|
3
13
|
let timedOut = false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type QuestionAnswer } from "./ask-question.js";
|
|
1
2
|
import type { ReadEntry } from "../tools/fs/safety.js";
|
|
2
3
|
import type { RepairBundle } from "../agents/repair-loop.js";
|
|
3
4
|
import type { WorkspaceHandle } from "./remote-env.js";
|
|
@@ -68,6 +69,7 @@ export type ResumeOutcome = {
|
|
|
68
69
|
decision: "allow" | "deny";
|
|
69
70
|
updatedInput?: unknown;
|
|
70
71
|
reason?: string;
|
|
72
|
+
answer?: QuestionAnswer;
|
|
71
73
|
} | {
|
|
72
74
|
gate: "resource_limit";
|
|
73
75
|
decision: "continue";
|
|
@@ -180,6 +182,7 @@ export interface ResolvedOutcome {
|
|
|
180
182
|
boundCallId: string;
|
|
181
183
|
decision: "allow" | "deny" | "approve" | "reject" | "edit";
|
|
182
184
|
updatedInput?: unknown;
|
|
185
|
+
answer?: QuestionAnswer;
|
|
183
186
|
}
|
|
184
187
|
export type ReopenReason = "env_failed" | "tool_unavailable";
|
|
185
188
|
export interface ResolveExpectation {
|
|
@@ -241,10 +244,10 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
|
|
|
241
244
|
export declare class CheckpointError extends Error {
|
|
242
245
|
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch";
|
|
243
246
|
readonly detail?: {
|
|
244
|
-
field?: "boundCallId" | "boundInputHash";
|
|
247
|
+
field?: "boundCallId" | "boundInputHash" | "answer";
|
|
245
248
|
} | undefined;
|
|
246
249
|
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string, detail?: {
|
|
247
|
-
field?: "boundCallId" | "boundInputHash";
|
|
250
|
+
field?: "boundCallId" | "boundInputHash" | "answer";
|
|
248
251
|
} | undefined);
|
|
249
252
|
}
|
|
250
253
|
export interface CheckpointStore {
|
|
@@ -223,9 +223,12 @@ export function winnerFromOutcome(outcome) {
|
|
|
223
223
|
}
|
|
224
224
|
if (outcome.gate !== "policy_ask")
|
|
225
225
|
return undefined;
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
return {
|
|
227
|
+
boundCallId: outcome.boundCallId,
|
|
228
|
+
decision: outcome.decision,
|
|
229
|
+
...(outcome.updatedInput === undefined ? {} : { updatedInput: outcome.updatedInput }),
|
|
230
|
+
...(outcome.answer === undefined ? {} : { answer: outcome.answer }),
|
|
231
|
+
};
|
|
229
232
|
}
|
|
230
233
|
export function validatePendingSteer(steer) {
|
|
231
234
|
if (sanitizeUntrustedText(steer.text) !== steer.text) {
|
|
@@ -5,7 +5,7 @@ import { engineVersion } from "../version.js";
|
|
|
5
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
6
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
7
7
|
import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, isCompactionManualCancel, isCompactionWalltimeAbort, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
|
|
8
|
-
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
8
|
+
import { ASK_USER_QUESTION_TOOL_NAME, QUESTION_AWAITS_RESUME } from "../ask-question.js";
|
|
9
9
|
import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
|
|
10
10
|
import { emitTrace } from "../trace.js";
|
|
11
11
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
@@ -38,7 +38,7 @@ import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
|
38
38
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
39
39
|
import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
|
|
40
40
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
41
|
-
import { discloseDroppedPending, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
41
|
+
import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
42
42
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
43
43
|
import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
|
|
44
44
|
import { DEFAULT_MAX_TURNS } from "../../config/defaults.js";
|
|
@@ -135,7 +135,24 @@ function deepJsonEqual(a, b) {
|
|
|
135
135
|
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
|
|
136
136
|
}
|
|
137
137
|
function sameWinner(a, b) {
|
|
138
|
-
return a.boundCallId === b.boundCallId &&
|
|
138
|
+
return (a.boundCallId === b.boundCallId &&
|
|
139
|
+
a.decision === b.decision &&
|
|
140
|
+
deepJsonEqual(a.updatedInput, b.updatedInput) &&
|
|
141
|
+
deepJsonEqual(a.answer, b.answer));
|
|
142
|
+
}
|
|
143
|
+
function pendingContentAskCallId(cp) {
|
|
144
|
+
return cp.pendingAction.kind === "tool_approval" && cp.pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME
|
|
145
|
+
? cp.pendingAction.toolCallId
|
|
146
|
+
: undefined;
|
|
147
|
+
}
|
|
148
|
+
function answerFaceForRedeemedCall(answer, redeemedCallId, base) {
|
|
149
|
+
return async (req, signal) => {
|
|
150
|
+
if (req.toolCallId === redeemedCallId)
|
|
151
|
+
return answer;
|
|
152
|
+
if (base !== undefined)
|
|
153
|
+
return base(req, signal);
|
|
154
|
+
throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
|
|
155
|
+
};
|
|
139
156
|
}
|
|
140
157
|
function writeFamilyOfCanonical(name) {
|
|
141
158
|
if (name === "TaskCreate" || name === "TaskUpdate")
|
|
@@ -1559,6 +1576,32 @@ export class Runner {
|
|
|
1559
1576
|
let notificationIdent = () => ({});
|
|
1560
1577
|
let notificationLaneLive = true;
|
|
1561
1578
|
let notificationSessionId;
|
|
1579
|
+
let descendantAnchorDisclosed = false;
|
|
1580
|
+
const parkTaskNotification = (payload, priority) => {
|
|
1581
|
+
if (notificationSessionId !== undefined)
|
|
1582
|
+
this.pendingSessionNotifications.pend(notificationSessionId, payload);
|
|
1583
|
+
if (!isDelegatedAgentTerminal(payload))
|
|
1584
|
+
return;
|
|
1585
|
+
const uplink = internals?.parentNotify;
|
|
1586
|
+
if (uplink !== undefined) {
|
|
1587
|
+
try {
|
|
1588
|
+
uplink(payload, { priority });
|
|
1589
|
+
}
|
|
1590
|
+
catch (e) {
|
|
1591
|
+
this.deps.onError?.(e, { phase: "degraded", sessionId: notificationSessionId ?? "", classification: "descendant-terminal-uplink" });
|
|
1592
|
+
}
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const rootAnchor = internals?.rootSessionId;
|
|
1596
|
+
if (rootAnchor !== undefined && rootAnchor !== notificationSessionId) {
|
|
1597
|
+
this.pendingSessionNotifications.pend(rootAnchor, payload);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (internals?.parentSessionId !== undefined && !descendantAnchorDisclosed) {
|
|
1601
|
+
descendantAnchorDisclosed = true;
|
|
1602
|
+
this.deps.onError?.(new Error(`background-agent terminal ${payload.task_id} parked on this run's own session — the delegation tree's root session is not reachable from this run (no uplink, no root anchor)`), { phase: "degraded", sessionId: notificationSessionId ?? "", classification: "descendant-terminal-unrouted" });
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1562
1605
|
const unsubscribeTaskNotifications = taskNotificationQueue.subscribe((item) => {
|
|
1563
1606
|
queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
|
|
1564
1607
|
if (notificationHarness) {
|
|
@@ -1567,14 +1610,12 @@ export class Runner {
|
|
|
1567
1610
|
? notificationHarness.followUp(xml, { provenance: "engine-note", enginePayload: item.payload })
|
|
1568
1611
|
: notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
|
|
1569
1612
|
void deliver.then(() => item.onDisposition?.("queued"), () => {
|
|
1570
|
-
|
|
1571
|
-
this.pendingSessionNotifications.pend(notificationSessionId, item.payload);
|
|
1613
|
+
parkTaskNotification(item.payload, item.priority);
|
|
1572
1614
|
item.onDisposition?.("parked");
|
|
1573
1615
|
});
|
|
1574
1616
|
}
|
|
1575
1617
|
else {
|
|
1576
|
-
|
|
1577
|
-
this.pendingSessionNotifications.pend(notificationSessionId, item.payload);
|
|
1618
|
+
parkTaskNotification(item.payload, item.priority);
|
|
1578
1619
|
item.onDisposition?.("parked");
|
|
1579
1620
|
}
|
|
1580
1621
|
});
|
|
@@ -1584,8 +1625,7 @@ export class Runner {
|
|
|
1584
1625
|
if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
|
|
1585
1626
|
return Promise.resolve("dropped_duplicate");
|
|
1586
1627
|
if (!notificationLaneLive) {
|
|
1587
|
-
|
|
1588
|
-
this.pendingSessionNotifications.pend(notificationSessionId, notification);
|
|
1628
|
+
parkTaskNotification(notification, opts?.priority ?? "later");
|
|
1589
1629
|
return Promise.resolve("parked");
|
|
1590
1630
|
}
|
|
1591
1631
|
return new Promise((resolve) => {
|
|
@@ -3240,6 +3280,20 @@ export class Runner {
|
|
|
3240
3280
|
throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
|
|
3241
3281
|
}
|
|
3242
3282
|
}
|
|
3283
|
+
const contentAskCallId = pendingContentAskCallId(cp);
|
|
3284
|
+
if (outcome.answer !== undefined && (contentAskCallId === undefined || outcome.decision !== "allow")) {
|
|
3285
|
+
throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
|
|
3286
|
+
? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
|
|
3287
|
+
: "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
|
|
3288
|
+
}
|
|
3289
|
+
if (contentAskCallId !== undefined && outcome.decision === "allow" && outcome.answer === undefined) {
|
|
3290
|
+
const liveFace = taskConfig.onQuestion ?? this.deps.onQuestion;
|
|
3291
|
+
if (liveFace === undefined || liveFace === QUESTION_AWAITS_RESUME) {
|
|
3292
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
|
|
3293
|
+
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
3294
|
+
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3243
3297
|
}
|
|
3244
3298
|
if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
3245
3299
|
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`);
|
|
@@ -3279,12 +3333,16 @@ export class Runner {
|
|
|
3279
3333
|
await internals.afterCheckpointResolve();
|
|
3280
3334
|
consumeFlipDone = true;
|
|
3281
3335
|
}
|
|
3336
|
+
const answerFace = outcome.gate === "policy_ask" && outcome.answer !== undefined
|
|
3337
|
+
? answerFaceForRedeemedCall(outcome.answer, outcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion)
|
|
3338
|
+
: undefined;
|
|
3282
3339
|
const spec = {
|
|
3283
3340
|
...taskConfig,
|
|
3284
3341
|
objective: "",
|
|
3285
3342
|
sessionId: cp.sessionId,
|
|
3286
3343
|
requireExistingSession: true,
|
|
3287
3344
|
preemptSignal: taskConfig.preemptSignal?.aborted ? undefined : taskConfig.preemptSignal,
|
|
3345
|
+
...(answerFace !== undefined ? { onQuestion: answerFace } : {}),
|
|
3288
3346
|
};
|
|
3289
3347
|
const reopenFn = store.reopen?.bind(store);
|
|
3290
3348
|
const onEnvRestoreFailed = reopenFn
|
|
@@ -6,5 +6,7 @@ export interface SecretEnvFinding {
|
|
|
6
6
|
confidence: RedactionConfidence;
|
|
7
7
|
source: RedactionSource;
|
|
8
8
|
}
|
|
9
|
+
export type ExactEnvNameVerdict = "credential" | "exempt" | "unknown";
|
|
10
|
+
export declare function classifyExactEnvName(key: string, credentialNames: ReadonlySet<string>, exemptNames: ReadonlySet<string>): ExactEnvNameVerdict;
|
|
9
11
|
export declare function isSecretEnvKey(key: string): boolean;
|
|
10
12
|
export declare function scrubSecretEnv(env: NodeJS.ProcessEnv, findings?: SecretEnvFinding[]): NodeJS.ProcessEnv;
|
package/dist/core/secret-env.js
CHANGED
|
@@ -4,16 +4,28 @@ 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 NON_SECRET_ENV_EXACT_NAMES = new Set([
|
|
8
|
+
"PARTITION_KEY", "SORT_KEY", "RANGE_KEY", "IDEMPOTENCY_KEY", "CACHE_KEY", "ROUTING_KEY",
|
|
9
|
+
]);
|
|
7
10
|
const SECRET_ENV_KIND_CONFIDENCE = {
|
|
8
11
|
"exact-name": "high",
|
|
9
12
|
"suffix-rule": "medium",
|
|
10
13
|
};
|
|
14
|
+
export function classifyExactEnvName(key, credentialNames, exemptNames) {
|
|
15
|
+
const upper = key.toUpperCase();
|
|
16
|
+
if (credentialNames.has(upper))
|
|
17
|
+
return "credential";
|
|
18
|
+
if (exemptNames.has(upper))
|
|
19
|
+
return "exempt";
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
11
22
|
function classifySecretEnvKey(key) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (SECRET_ENV_EXACT_NAMES.has(key.toUpperCase()))
|
|
23
|
+
const byExactName = classifyExactEnvName(key, SECRET_ENV_EXACT_NAMES, NON_SECRET_ENV_EXACT_NAMES);
|
|
24
|
+
if (byExactName === "credential")
|
|
15
25
|
return "exact-name";
|
|
16
|
-
|
|
26
|
+
if (byExactName === "exempt")
|
|
27
|
+
return undefined;
|
|
28
|
+
return SECRET_ENV_RE.test(key) ? "suffix-rule" : undefined;
|
|
17
29
|
}
|
|
18
30
|
export function isSecretEnvKey(key) {
|
|
19
31
|
return classifySecretEnvKey(key) !== undefined;
|
|
@@ -45,6 +45,7 @@ export declare function renderTaskNotificationXml(n: TaskNotificationPayload): s
|
|
|
45
45
|
export declare const MAX_PENDING_EVENTS_PER_TASK = 50;
|
|
46
46
|
export declare const MAX_PENDING_EVENTS_PER_SESSION = 200;
|
|
47
47
|
export declare const MAX_PENDING_SESSIONS = 100;
|
|
48
|
+
export declare function isDelegatedAgentTerminal(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
|
|
48
49
|
export interface DrainedPendingNotifications {
|
|
49
50
|
items: TaskNotificationPayload[];
|
|
50
51
|
dropped: Map<string, {
|
|
@@ -83,6 +83,9 @@ export const MAX_PENDING_EVENTS_PER_SESSION = 200;
|
|
|
83
83
|
export const MAX_PENDING_SESSIONS = 100;
|
|
84
84
|
const MAX_TOMBSTONES = 5_000;
|
|
85
85
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "killed", "cancelled"]);
|
|
86
|
+
export function isDelegatedAgentTerminal(n) {
|
|
87
|
+
return n.task_type === "background_agent" && TERMINAL_STATUSES.has(n.status);
|
|
88
|
+
}
|
|
86
89
|
export class PendingSessionNotifications {
|
|
87
90
|
sessions = new Map();
|
|
88
91
|
droppedSessions = 0;
|
package/dist/tools/fs/fs-read.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
|
-
import { sha256, resolveKey, violationText, violationDetails, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, } from "./safety.js";
|
|
3
|
+
import { sha256, resolveKey, violationText, violationDetails, binaryMagicFormat, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, } from "./safety.js";
|
|
4
4
|
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";
|
|
@@ -166,6 +166,11 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
166
166
|
if (pdfMagicMatches(readBin.value)) {
|
|
167
167
|
return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities));
|
|
168
168
|
}
|
|
169
|
+
const magicFormat = binaryMagicFormat(readBin.value);
|
|
170
|
+
if (magicFormat !== undefined) {
|
|
171
|
+
return errorResult(`Error (Read): "${path}" appears to be a binary file (${magicFormat}, identified by its file signature — whatever its extension says); ` +
|
|
172
|
+
`this tool reads UTF-8 and BOM-marked UTF-16LE text only. Inspect or convert it with bash instead.`);
|
|
173
|
+
}
|
|
169
174
|
const decoded = decodeTextBytes(readBin.value);
|
|
170
175
|
if (decoded.malformed) {
|
|
171
176
|
return errorResult(`Error (Read): "${path}" has a UTF-16 BOM but a truncated (odd-length) body — the file is corrupt or mis-labelled; repair/convert it with bash (e.g. \`iconv\`) first.`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
4
|
-
import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences,
|
|
4
|
+
import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, READ_REFUSED_ESCAPE_HINT, } from "./safety.js";
|
|
5
5
|
import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
|
|
6
6
|
import { MAX_EDIT_BYTES, formatByteSize, decodeEditBytes, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, } from "./fs-shared.js";
|
|
7
7
|
async function gateToolWrite(hook, tool, path, key, content) {
|
|
@@ -136,7 +136,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
136
136
|
return errorResult(ipynb);
|
|
137
137
|
const notRead = requireRead(state, r.key);
|
|
138
138
|
if (notRead)
|
|
139
|
-
return errorResult(await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal));
|
|
139
|
+
return errorResult(await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT));
|
|
140
140
|
const readBin = await env.readBinaryFile(r.key, ctx.signal);
|
|
141
141
|
if (!readBin.ok)
|
|
142
142
|
return errorResult(`Error (Edit): cannot read "${path}": ${readBin.error.message}`);
|
|
@@ -249,7 +249,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
249
249
|
if (exists.value) {
|
|
250
250
|
const notRead = requireRead(state, r.key);
|
|
251
251
|
if (notRead) {
|
|
252
|
-
return errorResult(await notReadRefusalText(env, "Write", r.key, notRead, ctx.signal,
|
|
252
|
+
return errorResult(await notReadRefusalText(env, "Write", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT));
|
|
253
253
|
}
|
|
254
254
|
const readBin = await env.readBinaryFile(r.key, ctx.signal);
|
|
255
255
|
if (!readBin.ok)
|
|
@@ -334,7 +334,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
|
|
|
334
334
|
return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
|
|
335
335
|
const notRead = requireRead(state, r.key);
|
|
336
336
|
if (notRead)
|
|
337
|
-
return errorResult(await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal));
|
|
337
|
+
return errorResult(await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT));
|
|
338
338
|
const readBin = await env.readBinaryFile(r.key, ctx.signal);
|
|
339
339
|
if (!readBin.ok)
|
|
340
340
|
return errorResult(`Error (NotebookEdit): cannot read "${notebook_path}": ${readBin.error.message}`);
|
|
@@ -30,6 +30,16 @@ export declare function foldRedundantPathSegments(p: string): string;
|
|
|
30
30
|
export declare function hasBinaryExtension(path: string): boolean;
|
|
31
31
|
export declare function imageMimeForRead(path: string): string | undefined;
|
|
32
32
|
export declare function imageMagicMatches(bytes: Uint8Array, mimeType: string): boolean;
|
|
33
|
+
export interface BinaryMagicSegment {
|
|
34
|
+
readonly offset: number;
|
|
35
|
+
readonly bytes: readonly number[];
|
|
36
|
+
}
|
|
37
|
+
export interface BinaryMagicSignature {
|
|
38
|
+
readonly format: string;
|
|
39
|
+
readonly segments: readonly BinaryMagicSegment[];
|
|
40
|
+
}
|
|
41
|
+
export declare const BINARY_MAGIC_SIGNATURES: readonly BinaryMagicSignature[];
|
|
42
|
+
export declare function binaryMagicFormat(bytes: Uint8Array): string | undefined;
|
|
33
43
|
export declare function isBinaryContent(sample: string): boolean;
|
|
34
44
|
export declare function isUncPath(path: string): boolean;
|
|
35
45
|
export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRootsCanonical?: readonly string[], exactFileReadExemption?: (canonicalKey: string) => boolean): Promise<{
|
|
@@ -59,7 +69,7 @@ export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to t
|
|
|
59
69
|
export declare function requireRead(state: ReadFileState, key: string): FsViolation | undefined;
|
|
60
70
|
export declare const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused \u2014 read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
|
|
61
71
|
export declare const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view \u2014 the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
|
|
62
|
-
export declare const
|
|
72
|
+
export declare const READ_REFUSED_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead \u2014 e.g. `rm` + rewrite, or `iconv`.)";
|
|
63
73
|
export declare function checkNoChange(oldString: string, newString: string): FsViolation | undefined;
|
|
64
74
|
export declare function checkStale(entry: ReadEntry | undefined, currentHash: string): FsViolation | undefined;
|
|
65
75
|
export declare function countOccurrences(haystack: string, needle: string): number;
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -150,6 +150,89 @@ export function imageMagicMatches(bytes, mimeType) {
|
|
|
150
150
|
return false;
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
+
const asciiBytes = (s) => Array.from(s, (c) => c.charCodeAt(0));
|
|
154
|
+
export const BINARY_MAGIC_SIGNATURES = [
|
|
155
|
+
{ format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x03, 0x04] }] },
|
|
156
|
+
{ format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x05, 0x06] }] },
|
|
157
|
+
{ format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x07, 0x08] }] },
|
|
158
|
+
{ format: "gzip archive", segments: [{ offset: 0, bytes: [0x1f, 0x8b, 0x08] }] },
|
|
159
|
+
...[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39].map((level) => ({
|
|
160
|
+
format: "bzip2 archive",
|
|
161
|
+
segments: [{ offset: 0, bytes: [0x42, 0x5a, 0x68, level] }],
|
|
162
|
+
})),
|
|
163
|
+
{ format: "xz archive", segments: [{ offset: 0, bytes: [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00] }] },
|
|
164
|
+
{ format: "zstd archive", segments: [{ offset: 0, bytes: [0x28, 0xb5, 0x2f, 0xfd] }] },
|
|
165
|
+
{ format: "lz4 archive", segments: [{ offset: 0, bytes: [0x04, 0x22, 0x4d, 0x18] }] },
|
|
166
|
+
{ format: "compress archive", segments: [{ offset: 0, bytes: [0x1f, 0x9d] }] },
|
|
167
|
+
{ format: "7-Zip archive", segments: [{ offset: 0, bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c] }] },
|
|
168
|
+
{ format: "RAR archive", segments: [{ offset: 0, bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00] }] },
|
|
169
|
+
{ format: "RAR archive", segments: [{ offset: 0, bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00] }] },
|
|
170
|
+
{ format: "tar archive", segments: [{ offset: 257, bytes: asciiBytes("ustar") }] },
|
|
171
|
+
{ format: "ar archive (static library or Debian package)", segments: [{ offset: 0, bytes: asciiBytes("!<arch>\n") }] },
|
|
172
|
+
{ format: "RPM package", segments: [{ offset: 0, bytes: [0xed, 0xab, 0xee, 0xdb] }] },
|
|
173
|
+
{ format: "ISO 9660 disc image", segments: [{ offset: 32769, bytes: asciiBytes("CD001") }] },
|
|
174
|
+
{ format: "PNG image", segments: [{ offset: 0, bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] }] },
|
|
175
|
+
{ format: "JPEG image", segments: [{ offset: 0, bytes: [0xff, 0xd8, 0xff] }] },
|
|
176
|
+
{ format: "GIF image", segments: [{ offset: 0, bytes: asciiBytes("GIF87a") }] },
|
|
177
|
+
{ format: "GIF image", segments: [{ offset: 0, bytes: asciiBytes("GIF89a") }] },
|
|
178
|
+
{ format: "TIFF image", segments: [{ offset: 0, bytes: [0x49, 0x49, 0x2a, 0x00] }] },
|
|
179
|
+
{ format: "TIFF image", segments: [{ offset: 0, bytes: [0x4d, 0x4d, 0x00, 0x2a] }] },
|
|
180
|
+
{ format: "BMP image", segments: [{ offset: 0, bytes: asciiBytes("BM") }, { offset: 6, bytes: [0x00, 0x00, 0x00, 0x00] }] },
|
|
181
|
+
{ format: "ICO image", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0x00] }] },
|
|
182
|
+
{ format: "Photoshop document", segments: [{ offset: 0, bytes: asciiBytes("8BPS") }] },
|
|
183
|
+
{ format: "WebP image", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("WEBP") }] },
|
|
184
|
+
{ format: "RIFF audio (WAV)", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("WAVE") }] },
|
|
185
|
+
{ format: "RIFF video (AVI)", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("AVI ") }] },
|
|
186
|
+
{ format: "AIFF audio", segments: [{ offset: 0, bytes: asciiBytes("FORM") }, { offset: 8, bytes: asciiBytes("AIFF") }] },
|
|
187
|
+
{ format: "Ogg container", segments: [{ offset: 0, bytes: asciiBytes("OggS") }, { offset: 4, bytes: [0x00] }] },
|
|
188
|
+
{ format: "FLAC audio", segments: [{ offset: 0, bytes: asciiBytes("fLaC") }] },
|
|
189
|
+
...[0x02, 0x03, 0x04].map((major) => ({
|
|
190
|
+
format: "MP3 audio (ID3 tag)",
|
|
191
|
+
segments: [{ offset: 0, bytes: [0x49, 0x44, 0x33, major] }],
|
|
192
|
+
})),
|
|
193
|
+
{ format: "ISO base media container (MP4/MOV/HEIC)", segments: [{ offset: 4, bytes: asciiBytes("ftyp") }] },
|
|
194
|
+
{ format: "Matroska/WebM container", segments: [{ offset: 0, bytes: [0x1a, 0x45, 0xdf, 0xa3] }] },
|
|
195
|
+
{ format: "ASF container (WMV/WMA)", segments: [{ offset: 0, bytes: [0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c] }] },
|
|
196
|
+
{ format: "FLV video", segments: [{ offset: 0, bytes: [0x46, 0x4c, 0x56, 0x01] }] },
|
|
197
|
+
{ format: "MPEG program stream", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0xba] }] },
|
|
198
|
+
{ format: "MPEG video stream", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0xb3] }] },
|
|
199
|
+
{ format: "ELF binary", segments: [{ offset: 0, bytes: [0x7f, 0x45, 0x4c, 0x46] }] },
|
|
200
|
+
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xfe, 0xed, 0xfa, 0xce] }] },
|
|
201
|
+
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xfe, 0xed, 0xfa, 0xcf] }] },
|
|
202
|
+
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xce, 0xfa, 0xed, 0xfe] }] },
|
|
203
|
+
{ format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xcf, 0xfa, 0xed, 0xfe] }] },
|
|
204
|
+
{ format: "Java class file or Mach-O universal binary", segments: [{ offset: 0, bytes: [0xca, 0xfe, 0xba, 0xbe] }] },
|
|
205
|
+
{ format: "WebAssembly module", segments: [{ offset: 0, bytes: [0x00, 0x61, 0x73, 0x6d] }] },
|
|
206
|
+
{ format: "OLE compound document (Office 97-2003 or MSI)", segments: [{ offset: 0, bytes: [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1] }] },
|
|
207
|
+
{ format: "SQLite database", segments: [{ offset: 0, bytes: [...asciiBytes("SQLite format 3"), 0x00] }] },
|
|
208
|
+
{ format: "WOFF font", segments: [{ offset: 0, bytes: asciiBytes("wOFF") }] },
|
|
209
|
+
{ format: "WOFF2 font", segments: [{ offset: 0, bytes: asciiBytes("wOF2") }] },
|
|
210
|
+
{ format: "OpenType font", segments: [{ offset: 0, bytes: asciiBytes("OTTO") }] },
|
|
211
|
+
{ format: "TrueType font", segments: [{ offset: 0, bytes: [0x00, 0x01, 0x00, 0x00] }] },
|
|
212
|
+
{ format: "TrueType font collection", segments: [{ offset: 0, bytes: asciiBytes("ttcf") }] },
|
|
213
|
+
];
|
|
214
|
+
export function binaryMagicFormat(bytes) {
|
|
215
|
+
for (const signature of BINARY_MAGIC_SIGNATURES) {
|
|
216
|
+
let matched = true;
|
|
217
|
+
for (const segment of signature.segments) {
|
|
218
|
+
if (bytes.length < segment.offset + segment.bytes.length) {
|
|
219
|
+
matched = false;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
for (let i = 0; i < segment.bytes.length; i++) {
|
|
223
|
+
if (bytes[segment.offset + i] !== segment.bytes[i]) {
|
|
224
|
+
matched = false;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!matched)
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (matched)
|
|
232
|
+
return signature.format;
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
153
236
|
export function isBinaryContent(sample) {
|
|
154
237
|
if (sample.length === 0)
|
|
155
238
|
return false;
|
|
@@ -297,7 +380,7 @@ export function requireRead(state, key) {
|
|
|
297
380
|
}
|
|
298
381
|
export const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused — read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
|
|
299
382
|
export const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view — the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
|
|
300
|
-
export const
|
|
383
|
+
export const READ_REFUSED_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead — e.g. `rm` + rewrite, or `iconv`.)";
|
|
301
384
|
export function checkNoChange(oldString, newString) {
|
|
302
385
|
if (oldString === newString) {
|
|
303
386
|
return { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.0",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"gate:single-mint": "node scripts/verify-single-mint.mjs"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@modelcontextprotocol/sdk": "1.
|
|
70
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
71
71
|
"typebox": "1.1.39"
|
|
72
72
|
},
|
|
73
73
|
"optionalDependencies": {
|