@sema-agent/core 5.4.0 → 5.6.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 +27 -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/arg-summary.d.ts +8 -0
- package/dist/core/arg-summary.js +27 -9
- package/dist/core/runner/prepare-task.js +2 -2
- 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/untrusted-egress.d.ts +2 -1
- package/dist/core/untrusted-egress.js +5 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -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/dist/tools/scheduler-tools.js +48 -15
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.6.0 (2026-08-03)
|
|
4
|
+
|
|
5
|
+
_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._
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
11
|
+
- 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).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `@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).
|
|
16
|
+
|
|
17
|
+
## 5.5.0 (2026-08-03)
|
|
18
|
+
|
|
19
|
+
_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._
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
3
30
|
## 5.4.0 (2026-08-03)
|
|
4
31
|
|
|
5
32
|
_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._
|
|
@@ -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,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
|
},
|
|
@@ -1469,9 +1469,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1469
1469
|
if (spec.interactiveTools === true || (spec.interactiveTools !== false && (onQuestion !== undefined || durableQuestionFace)))
|
|
1470
1470
|
tools.push(createAskUserQuestionTool(onQuestion, { principal: spec.principal, sourceTaskId: sessionId }));
|
|
1471
1471
|
if (spec.handsReadOnly !== true) {
|
|
1472
|
-
const
|
|
1472
|
+
const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
|
|
1473
1473
|
tools.push(...createSchedulerTools(executionEnv, {
|
|
1474
|
-
scope:
|
|
1474
|
+
scope: sessionScope,
|
|
1475
1475
|
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
1476
1476
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1477
1477
|
requestStopAfterTurn,
|
|
@@ -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
|
}
|
|
@@ -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);
|
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";
|
|
@@ -172,7 +173,8 @@ export { FileBackgroundAgentStore, type FileBackgroundAgentStoreOptions } from "
|
|
|
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
|
@@ -157,6 +157,7 @@ export { FileBackgroundAgentStore } from "./stores/file/background-agent-store.j
|
|
|
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
|
@@ -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." };
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.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": {
|