@sema-agent/core 7.16.0 → 7.17.1
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 +209 -0
- package/dist/agents/builtin-agents.d.ts +3 -3
- package/dist/agents/cross-session-envelope.d.ts +0 -1
- package/dist/agents/cross-session-envelope.js +0 -1
- package/dist/agents/peer-notices.d.ts +0 -9
- package/dist/agents/peer-notices.js +0 -63
- package/dist/agents/subagent.js +7 -15
- package/dist/brain/request-params.d.ts +2 -2
- package/dist/config/defaults.d.ts +0 -7
- package/dist/config/defaults.js +0 -2
- package/dist/core/artifact-host.d.ts +218 -0
- package/dist/core/artifact-host.js +92 -0
- package/dist/core/ask-origin.d.ts +29 -3
- package/dist/core/ask-origin.js +17 -9
- package/dist/core/ask-question.d.ts +25 -0
- package/dist/core/ask-question.js +4 -0
- package/dist/core/ask-unresolvable-notice.d.ts +30 -21
- package/dist/core/ask-unresolvable-notice.js +16 -8
- package/dist/core/auto-compaction.d.ts +10 -10
- package/dist/core/auto-mode-arming.d.ts +2 -2
- package/dist/core/background-agent-store.d.ts +5 -5
- package/dist/core/background-shell.d.ts +1 -1
- package/dist/core/checkpoint-execution-record.d.ts +0 -3
- package/dist/core/checkpoint-execution-record.js +0 -4
- package/dist/core/checkpoint-store.d.ts +71 -99
- package/dist/core/checkpoint-store.js +9 -16
- package/dist/core/context-edit.d.ts +36 -104
- package/dist/core/context-edit.js +37 -60
- package/dist/core/effective-path-target.d.ts +68 -8
- package/dist/core/effective-path-target.js +42 -30
- package/dist/core/engine-notice.d.ts +46 -32
- package/dist/core/env-budget.d.ts +31 -0
- package/dist/core/env-budget.js +39 -0
- package/dist/core/exec-output-tail.d.ts +5 -5
- package/dist/core/file-history-store.js +3 -6
- package/dist/core/fs-write-gate-policy.d.ts +1 -1
- package/dist/core/fs-write-gate-policy.js +3 -3
- package/dist/core/gate-lanes.js +9 -15
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hands-band.d.ts +1 -1
- package/dist/core/hooks.d.ts +18 -17
- package/dist/core/lsp-session.d.ts +1 -1
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/lsp.js +5 -4
- package/dist/core/mcp.d.ts +5 -5
- package/dist/core/mcp.js +4 -48
- package/dist/core/memory-engine/engine.js +2 -1
- package/dist/core/memory-engine/layout.d.ts +18 -6
- package/dist/core/memory-engine/layout.js +40 -21
- package/dist/core/memory-recall.d.ts +0 -8
- package/dist/core/memory.d.ts +1 -1
- package/dist/core/oracle-isolation.js +2 -2
- package/dist/core/parent-spec-seat.d.ts +66 -0
- package/dist/core/parent-spec-seat.js +47 -0
- package/dist/core/permission-rule-model.d.ts +11 -6
- package/dist/core/permission-rule-model.js +53 -37
- package/dist/core/permission-rules.js +8 -3
- package/dist/core/physical-path.d.ts +37 -0
- package/dist/core/physical-path.js +30 -0
- package/dist/core/protocol-naming.d.ts +2 -2
- package/dist/core/remote-env.d.ts +1 -1
- package/dist/core/retired-keys.d.ts +19 -0
- package/dist/core/retired-keys.js +15 -0
- package/dist/core/runner/active-skill-scope.js +3 -3
- package/dist/core/runner/assemble-result.d.ts +15 -13
- package/dist/core/runner/assemble-result.js +5 -4
- package/dist/core/runner/compaction-call-options.d.ts +25 -89
- package/dist/core/runner/contracts.d.ts +11 -10
- package/dist/core/runner/gate-exit.js +15 -1
- package/dist/core/runner/halt-attribution.d.ts +35 -0
- package/dist/core/runner/halt-attribution.js +16 -0
- package/dist/core/runner/memory-consolidation.d.ts +0 -2
- package/dist/core/runner/memory-consolidation.js +0 -1
- package/dist/core/runner/prepare-artifact.d.ts +52 -0
- package/dist/core/runner/prepare-artifact.js +63 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
- package/dist/core/runner/prepare-config-doors.d.ts +3 -3
- package/dist/core/runner/prepare-config-doors.js +5 -2
- package/dist/core/runner/prepare-context-lane.d.ts +2 -2
- package/dist/core/runner/prepare-context-lane.js +2 -13
- package/dist/core/runner/prepare-file-history.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.js +3 -52
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
- package/dist/core/runner/prepare-inherited-gate.js +7 -8
- package/dist/core/runner/prepare-memory.d.ts +4 -6
- package/dist/core/runner/prepare-memory.js +4 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
- package/dist/core/runner/prepare-policy-chain.js +15 -30
- package/dist/core/runner/prepare-prompt-assembly.js +1 -1
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
- package/dist/core/runner/prepare-suspend-saga.js +0 -2
- package/dist/core/runner/prepare-task.js +12 -11
- package/dist/core/runner/prepare-wiring-manifest.js +9 -7
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-internals-and-config.js +4 -0
- package/dist/core/runner/retired-runner-deps.d.ts +3 -0
- package/dist/core/runner/retired-runner-deps.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +5 -3
- package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
- package/dist/core/runner/run-settle-and-teardown.js +3 -1
- package/dist/core/runner/session-file-state-replay.js +3 -3
- package/dist/core/runner/session-rule-policy.js +3 -4
- package/dist/core/runner/stream-halt-verbs.js +1 -1
- package/dist/core/runner/stream-steer-verb.js +1 -4
- package/dist/core/runner/workspace-path.js +3 -0
- package/dist/core/runner-deps.d.ts +30 -49
- package/dist/core/safe-notify.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/sensitive-path-policy.js +11 -12
- package/dist/core/session.d.ts +2 -2
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +4 -3
- package/dist/core/spec-contract.js +5 -4
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/strategy-store.d.ts +8 -11
- package/dist/core/task-event.d.ts +20 -17
- package/dist/core/task-limits.d.ts +1 -1
- package/dist/core/task-notification.d.ts +5 -5
- package/dist/core/task-registry-agent.d.ts +5 -5
- package/dist/core/task-registry-monitor.d.ts +2 -2
- package/dist/core/task-registry-shared.d.ts +32 -28
- package/dist/core/task-registry-shared.js +4 -13
- package/dist/core/task-registry.d.ts +8 -8
- package/dist/core/task-result.d.ts +37 -37
- package/dist/core/task-spec.d.ts +5 -5
- package/dist/core/task-stream.d.ts +16 -11
- package/dist/core/tool-catalog-entries.d.ts +1 -0
- package/dist/core/tool-catalog-entries.js +6 -1
- package/dist/core/tool-conformance.d.ts +0 -2
- package/dist/core/tool-conformance.js +1 -3
- package/dist/core/tool-errors.d.ts +5 -0
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +66 -22
- package/dist/core/tool-policy.js +31 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +11 -11
- package/dist/core/tool-spec.d.ts +34 -38
- package/dist/core/tools.d.ts +2 -2
- package/dist/core/trace.d.ts +12 -15
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +4 -3
- package/dist/core/workflow-journal-store.d.ts +11 -11
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
- package/dist/engine/execution-env/node-execution-env.js +8 -5
- package/dist/engine/harness/agent-harness.d.ts +47 -4
- package/dist/engine/harness/agent-harness.js +52 -9
- package/dist/engine/loop/agent-loop.js +15 -8
- package/dist/engine/loop/types.d.ts +5 -0
- package/dist/index.d.ts +20 -14
- package/dist/index.js +16 -10
- package/dist/internal/harness.d.ts +1 -0
- package/dist/orchestration/run-spec.d.ts +3 -0
- package/dist/orchestration/run-spec.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +12 -6
- package/dist/orchestration/run-workflow-tool.js +23 -5
- package/dist/orchestration/workflow-primitives.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +1 -4
- package/dist/orchestration/workflow-script-store.js +9 -25
- package/dist/orchestration/workflow-types.d.ts +34 -4
- package/dist/orchestration/workflow-types.js +2 -0
- package/dist/orchestration/workflow.d.ts +39 -7
- package/dist/orchestration/workflow.js +237 -113
- package/dist/prompt-assembly/assemble.d.ts +2 -2
- package/dist/prompt-assembly/assemble.js +9 -9
- package/dist/prompt-assembly/composer.d.ts +6 -7
- package/dist/prompt-assembly/composer.js +10 -10
- package/dist/prompt-assembly/epoch.js +1 -1
- package/dist/prompt-assembly/event-registry.d.ts +0 -21
- package/dist/prompt-assembly/packs/sema-default.js +30 -30
- package/dist/prompt-assembly/types.d.ts +14 -14
- package/dist/prompts/coordinator.d.ts +0 -13
- package/dist/prompts/coordinator.js +0 -3
- package/dist/stores/cc/task-list-store.js +2 -10
- package/dist/stores/file/checkpoint-store.js +1 -5
- package/dist/stores/file/file-history-store.js +2 -2
- package/dist/stores/file/fs-atomic.d.ts +15 -18
- package/dist/stores/file/fs-atomic.js +4 -14
- package/dist/stores/file/mailbox-store.d.ts +7 -11
- package/dist/stores/file/mailbox-store.js +4 -11
- package/dist/stores/file/permission-rule-store.d.ts +4 -35
- package/dist/stores/file/permission-rule-store.js +4 -148
- package/dist/tools/artifact/artifact-text.d.ts +62 -0
- package/dist/tools/artifact/artifact-text.js +68 -0
- package/dist/tools/artifact/artifact-tool.d.ts +52 -0
- package/dist/tools/artifact/artifact-tool.js +351 -0
- package/dist/tools/artifact/local-stub.d.ts +26 -0
- package/dist/tools/artifact/local-stub.js +297 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +90 -8
- package/dist/tools/fs/bash-readonly-classifier.js +294 -70
- package/dist/tools/fs/fs-bash.d.ts +14 -10
- package/dist/tools/fs/fs-bash.js +50 -33
- package/dist/tools/fs/fs-search-tools.js +5 -2
- package/dist/tools/fs/fs-shared.d.ts +2 -0
- package/dist/tools/fs/fs-shared.js +3 -11
- package/dist/tools/fs/index.d.ts +4 -0
- package/dist/tools/fs/index.js +4 -1
- package/dist/tools/fs/safety.d.ts +117 -1
- package/dist/tools/fs/safety.js +131 -31
- package/dist/tools/fs/search.js +8 -6
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +119 -39
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import {} from "../../core/artifact-host.js";
|
|
5
|
+
import { physicalPathOf } from "../../core/physical-path.js";
|
|
6
|
+
import { acquireStoreDirLock } from "../../stores/file/fs-atomic.js";
|
|
7
|
+
import { normalizePublishedPath } from "./artifact-tool.js";
|
|
8
|
+
export const EVAL_STUB_URL_PREFIX = "eval-stub://artifact/";
|
|
9
|
+
export const LOCAL_STUB_MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
export const LOCAL_STUB_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
const LEDGER_FILE = "ledger.json";
|
|
12
|
+
const TMP_DIR = ".tmp";
|
|
13
|
+
const LOCK_FILE = "LOCK";
|
|
14
|
+
const RESERVED_NAMES = new Set([LEDGER_FILE, TMP_DIR, LOCK_FILE].map((n) => n.toLowerCase()));
|
|
15
|
+
const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
16
|
+
const ATTEMPT_WINDOW = 512;
|
|
17
|
+
const emptyLedger = () => ({ v: 1, artifacts: {}, attempts: {} });
|
|
18
|
+
const isRow = (x, key) => {
|
|
19
|
+
if (x === null || typeof x !== "object")
|
|
20
|
+
return false;
|
|
21
|
+
const r = x;
|
|
22
|
+
if (typeof r.slug !== "string" || r.slug !== key || !SLUG_RE.test(r.slug) || r.slug.startsWith(".") || RESERVED_NAMES.has(r.slug.toLowerCase()))
|
|
23
|
+
return false;
|
|
24
|
+
if (typeof r.url !== "string" || !Number.isSafeInteger(r.version) || r.version < 1 || !Array.isArray(r.files))
|
|
25
|
+
return false;
|
|
26
|
+
return r.files.every((f) => f !== null && typeof f === "object" && typeof f.path === "string" && normalizePublishedPath(f.path).ok && Number.isSafeInteger(f.bytes));
|
|
27
|
+
};
|
|
28
|
+
function parseLedger(text, path) {
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`artifact stub ledger is corrupt (not JSON): ${path}`);
|
|
35
|
+
}
|
|
36
|
+
const l = parsed;
|
|
37
|
+
if (l === null || typeof l !== "object" || l.v !== 1 || l.artifacts === null || typeof l.artifacts !== "object" || l.attempts === null || typeof l.attempts !== "object") {
|
|
38
|
+
throw new Error(`artifact stub ledger is corrupt (unexpected shape): ${path}`);
|
|
39
|
+
}
|
|
40
|
+
for (const [key, row] of Object.entries(l.artifacts))
|
|
41
|
+
if (!isRow(row, key))
|
|
42
|
+
throw new Error(`artifact stub ledger is corrupt (bad artifact row): ${path}`);
|
|
43
|
+
return { v: 1, artifacts: { ...l.artifacts }, attempts: { ...l.attempts } };
|
|
44
|
+
}
|
|
45
|
+
function fsyncDirBestEffort(dir) {
|
|
46
|
+
try {
|
|
47
|
+
const fd = openSync(dir, "r");
|
|
48
|
+
try {
|
|
49
|
+
fsyncSync(fd);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function fsyncTreeBestEffort(dir) {
|
|
59
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }))
|
|
60
|
+
if (entry.isDirectory())
|
|
61
|
+
fsyncTreeBestEffort(join(dir, entry.name));
|
|
62
|
+
fsyncDirBestEffort(dir);
|
|
63
|
+
}
|
|
64
|
+
function writeFsync(path, bytes) {
|
|
65
|
+
const fd = openSync(path, "w", 0o600);
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(fd, bytes);
|
|
68
|
+
fsyncSync(fd);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
closeSync(fd);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function slugBase(fileName) {
|
|
75
|
+
const dot = fileName.lastIndexOf(".");
|
|
76
|
+
const stem = (dot > 0 ? fileName.slice(0, dot) : fileName).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
77
|
+
return stem.length > 0 ? stem : "artifact";
|
|
78
|
+
}
|
|
79
|
+
export class LocalArtifactStub {
|
|
80
|
+
dir;
|
|
81
|
+
#mint = new Map();
|
|
82
|
+
#chain = Promise.resolve();
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
if (typeof opts?.dir !== "string" || opts.dir.length === 0)
|
|
85
|
+
throw new Error("localArtifactStub: `dir` is required");
|
|
86
|
+
this.dir = resolve(opts.dir);
|
|
87
|
+
}
|
|
88
|
+
capabilities() {
|
|
89
|
+
return { plane: "local-stub", actions: ["publish"], defaultVisibility: "private", maxFileBytes: LOCAL_STUB_MAX_FILE_BYTES, maxTotalBytes: LOCAL_STUB_MAX_TOTAL_BYTES, multiFile: false, storageDir: this.dir };
|
|
90
|
+
}
|
|
91
|
+
onMount(runId) {
|
|
92
|
+
this.#mint.set(runId, new Set());
|
|
93
|
+
}
|
|
94
|
+
ownMinted(runId) {
|
|
95
|
+
return new Set(this.#mint.get(runId) ?? []);
|
|
96
|
+
}
|
|
97
|
+
publish(req, ctx) {
|
|
98
|
+
const run = this.#chain.then(() => this.#publishLocked(req, ctx));
|
|
99
|
+
this.#chain = run.then(() => undefined, () => undefined);
|
|
100
|
+
return run;
|
|
101
|
+
}
|
|
102
|
+
#ledgerPath() {
|
|
103
|
+
return join(this.dir, LEDGER_FILE);
|
|
104
|
+
}
|
|
105
|
+
#loadLedger() {
|
|
106
|
+
const p = this.#ledgerPath();
|
|
107
|
+
if (!existsSync(p))
|
|
108
|
+
return emptyLedger();
|
|
109
|
+
return parseLedger(readFileSync(p, "utf8"), p);
|
|
110
|
+
}
|
|
111
|
+
#versionDir(slug, version) {
|
|
112
|
+
return join(this.dir, slug, `v${version}`);
|
|
113
|
+
}
|
|
114
|
+
#snapshotOf(row) {
|
|
115
|
+
const base = this.#versionDir(row.slug, row.version);
|
|
116
|
+
const realBase = physicalPathOf(base);
|
|
117
|
+
const files = row.files.map((f) => {
|
|
118
|
+
const target = physicalPathOf(resolve(base, ...f.path.split("/")));
|
|
119
|
+
if (!target.startsWith(realBase + sep))
|
|
120
|
+
throw new Error(`artifact stub ledger is corrupt (a file resolves outside its version directory): ${this.#ledgerPath()}`);
|
|
121
|
+
return { path: f.path, content: new Uint8Array(readFileSync(target)), ...(f.mediaType !== undefined ? { mediaType: f.mediaType } : {}) };
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
files,
|
|
125
|
+
...(row.title !== undefined ? { title: row.title } : {}),
|
|
126
|
+
...(row.description !== undefined ? { description: row.description } : {}),
|
|
127
|
+
...(row.favicon !== undefined ? { favicon: row.favicon } : {}),
|
|
128
|
+
...(row.label !== undefined ? { label: row.label } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
#mintSlug(ledger, requested, firstFile) {
|
|
132
|
+
const fold = requested?.toLowerCase();
|
|
133
|
+
if (requested !== undefined && fold !== undefined && SLUG_RE.test(requested) && !RESERVED_NAMES.has(fold) && !requested.startsWith(".") && !Object.keys(ledger.artifacts).some((s) => s.toLowerCase() === fold))
|
|
134
|
+
return requested;
|
|
135
|
+
const base = slugBase(firstFile);
|
|
136
|
+
const taken = new Set(Object.keys(ledger.artifacts).map((s) => s.toLowerCase()));
|
|
137
|
+
for (;;) {
|
|
138
|
+
const slug = `${base}-${randomBytes(3).toString("hex")}`;
|
|
139
|
+
if (!taken.has(slug))
|
|
140
|
+
return slug;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#commitLedger(next) {
|
|
144
|
+
const ledgerTmp = join(this.dir, `${LEDGER_FILE}.${randomBytes(4).toString("hex")}.tmp`);
|
|
145
|
+
try {
|
|
146
|
+
const text = JSON.stringify(next, null, 2);
|
|
147
|
+
writeFsync(ledgerTmp, text);
|
|
148
|
+
const verified = parseLedger(readFileSync(ledgerTmp, "utf8"), ledgerTmp);
|
|
149
|
+
if (JSON.stringify(verified) !== JSON.stringify(next))
|
|
150
|
+
throw new Error("artifact stub ledger did not read back as written");
|
|
151
|
+
renameSync(ledgerTmp, this.#ledgerPath());
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
try {
|
|
155
|
+
rmSync(ledgerTmp, { force: true });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
fsyncDirBestEffort(this.dir);
|
|
162
|
+
}
|
|
163
|
+
#withAttempt(ledger, attemptId, attempt) {
|
|
164
|
+
const entries = Object.entries(ledger.attempts).filter(([k]) => k !== attemptId);
|
|
165
|
+
while (entries.length >= ATTEMPT_WINDOW)
|
|
166
|
+
entries.shift();
|
|
167
|
+
entries.push([attemptId, attempt]);
|
|
168
|
+
return Object.fromEntries(entries);
|
|
169
|
+
}
|
|
170
|
+
#replayOf(ledger, attempt) {
|
|
171
|
+
if ("ref" in attempt)
|
|
172
|
+
return { ok: true, ref: attempt.ref };
|
|
173
|
+
if (attempt.code === "conflict") {
|
|
174
|
+
const row = ledger.artifacts[attempt.slug];
|
|
175
|
+
return { ok: false, code: "conflict", currentVersion: row !== undefined ? String(row.version) : attempt.currentVersion, current: row !== undefined ? this.#snapshotOf(row) : "reread" };
|
|
176
|
+
}
|
|
177
|
+
return { ok: false, code: attempt.code, message: attempt.message };
|
|
178
|
+
}
|
|
179
|
+
async #publishLocked(req, ctx) {
|
|
180
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
181
|
+
const lease = acquireStoreDirLock(this.dir, { label: "artifact stub", fileName: LOCK_FILE });
|
|
182
|
+
try {
|
|
183
|
+
const ledger = this.#loadLedger();
|
|
184
|
+
const replay = ledger.attempts[ctx.attemptId];
|
|
185
|
+
if (replay !== undefined)
|
|
186
|
+
return this.#replayOf(ledger, replay);
|
|
187
|
+
const decided = this.#decide(ledger, req, ctx);
|
|
188
|
+
if (!decided.ok) {
|
|
189
|
+
const attempt = decided.outcome.code === "conflict" ? { code: "conflict", slug: decided.slug ?? "", currentVersion: decided.outcome.currentVersion } : { code: decided.outcome.code, message: decided.outcome.message };
|
|
190
|
+
this.#commitLedger({ ...ledger, attempts: this.#withAttempt(ledger, ctx.attemptId, attempt) });
|
|
191
|
+
return decided.outcome;
|
|
192
|
+
}
|
|
193
|
+
const { slug, version, existing, files, mint } = decided;
|
|
194
|
+
const versionDir = this.#versionDir(slug, version);
|
|
195
|
+
const tmpDir = join(this.dir, TMP_DIR, `${slug}-v${version}-${randomBytes(4).toString("hex")}`);
|
|
196
|
+
let staged = false;
|
|
197
|
+
try {
|
|
198
|
+
if (existsSync(versionDir))
|
|
199
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
200
|
+
mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
|
|
201
|
+
for (const f of files) {
|
|
202
|
+
const target = join(tmpDir, ...f.path.split("/"));
|
|
203
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
204
|
+
writeFsync(target, f.content);
|
|
205
|
+
}
|
|
206
|
+
fsyncTreeBestEffort(tmpDir);
|
|
207
|
+
mkdirSync(dirname(versionDir), { recursive: true, mode: 0o700 });
|
|
208
|
+
renameSync(tmpDir, versionDir);
|
|
209
|
+
staged = true;
|
|
210
|
+
fsyncDirBestEffort(dirname(versionDir));
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
const url = `${EVAL_STUB_URL_PREFIX}${slug}`;
|
|
213
|
+
const row = {
|
|
214
|
+
slug,
|
|
215
|
+
url,
|
|
216
|
+
version,
|
|
217
|
+
...(req.title !== undefined ? { title: req.title } : existing?.title !== undefined ? { title: existing.title } : {}),
|
|
218
|
+
...(req.description !== undefined ? { description: req.description } : existing?.description !== undefined ? { description: existing.description } : {}),
|
|
219
|
+
...(req.favicon !== undefined ? { favicon: req.favicon } : existing?.favicon !== undefined ? { favicon: existing.favicon } : {}),
|
|
220
|
+
...(req.label !== undefined ? { label: req.label } : {}),
|
|
221
|
+
files: files.map((f) => ({ path: f.path, ...(f.mediaType !== undefined ? { mediaType: f.mediaType } : {}), bytes: f.content.byteLength })),
|
|
222
|
+
createdAt: existing?.createdAt ?? now,
|
|
223
|
+
updatedAt: now,
|
|
224
|
+
};
|
|
225
|
+
const ref = { url, slug, version: String(version), ...(row.title !== undefined ? { title: row.title } : {}) };
|
|
226
|
+
this.#commitLedger({ v: 1, artifacts: { ...ledger.artifacts, [slug]: row }, attempts: this.#withAttempt(ledger, ctx.attemptId, { ref }) });
|
|
227
|
+
mint.add(slug);
|
|
228
|
+
return { ok: true, ref };
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
for (const orphan of [tmpDir, ...(staged ? [versionDir] : [])]) {
|
|
232
|
+
try {
|
|
233
|
+
rmSync(orphan, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
lease.release();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
#decide(ledger, req, ctx) {
|
|
246
|
+
const refuse = (code, message, slug) => ({ ok: false, outcome: { ok: false, code, message }, ...(slug !== undefined ? { slug } : {}) });
|
|
247
|
+
if (req.removeFiles !== undefined && req.removeFiles.length > 0)
|
|
248
|
+
return refuse("refused", "the local stub does not apply removeFiles");
|
|
249
|
+
if (req.files.length === 0)
|
|
250
|
+
return refuse("refused", "a publish needs at least one file");
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
const files = [];
|
|
253
|
+
let total = 0;
|
|
254
|
+
for (const f of req.files) {
|
|
255
|
+
const norm = normalizePublishedPath(f.path);
|
|
256
|
+
if (!norm.ok)
|
|
257
|
+
return refuse("refused", `published path ${JSON.stringify(f.path).slice(0, 120)} ${norm.reason}`);
|
|
258
|
+
const fold = norm.path.toLowerCase();
|
|
259
|
+
if (seen.has(fold))
|
|
260
|
+
return refuse("refused", `published path ${JSON.stringify(norm.path).slice(0, 120)} collides with another entry`);
|
|
261
|
+
seen.add(fold);
|
|
262
|
+
total += f.content.byteLength;
|
|
263
|
+
if (f.content.byteLength > LOCAL_STUB_MAX_FILE_BYTES)
|
|
264
|
+
return refuse("payload_too_large", `${norm.path.slice(0, 120)} is ${f.content.byteLength} bytes; the limit is ${LOCAL_STUB_MAX_FILE_BYTES} bytes per file`);
|
|
265
|
+
files.push({ path: norm.path, content: f.content, ...(f.mediaType !== undefined ? { mediaType: f.mediaType } : {}) });
|
|
266
|
+
}
|
|
267
|
+
if (total > LOCAL_STUB_MAX_TOTAL_BYTES)
|
|
268
|
+
return refuse("payload_too_large", `the files total ${total} bytes; the limit is ${LOCAL_STUB_MAX_TOTAL_BYTES} bytes per version`);
|
|
269
|
+
const mint = this.#mint.get(ctx.runId) ?? new Set();
|
|
270
|
+
this.#mint.set(ctx.runId, mint);
|
|
271
|
+
if (req.url !== undefined) {
|
|
272
|
+
if (!req.url.startsWith(EVAL_STUB_URL_PREFIX))
|
|
273
|
+
return refuse("url_foreign", "not an eval-stub:// link this run published");
|
|
274
|
+
const slug = req.url.slice(EVAL_STUB_URL_PREFIX.length);
|
|
275
|
+
if (!mint.has(slug))
|
|
276
|
+
return refuse("url_foreign", "this run did not publish that artifact", slug);
|
|
277
|
+
const existing = ledger.artifacts[slug];
|
|
278
|
+
if (existing === undefined)
|
|
279
|
+
return refuse("not_found", "the artifact is no longer in the ledger", slug);
|
|
280
|
+
if (req.baseVersion !== undefined && req.baseVersion !== String(existing.version) && req.force !== true) {
|
|
281
|
+
return { ok: false, outcome: { ok: false, code: "conflict", currentVersion: String(existing.version), current: this.#snapshotOf(existing) }, slug };
|
|
282
|
+
}
|
|
283
|
+
return { ok: true, slug, version: existing.version + 1, existing, files, mint };
|
|
284
|
+
}
|
|
285
|
+
return { ok: true, slug: this.#mintSlug(ledger, req.slug, files[0].path), version: 1, existing: undefined, files, mint };
|
|
286
|
+
}
|
|
287
|
+
rows() {
|
|
288
|
+
return Object.values(this.#loadLedger().artifacts).map((r) => ({ url: r.url, slug: r.slug, version: String(r.version), ...(r.title !== undefined ? { title: r.title } : {}) }));
|
|
289
|
+
}
|
|
290
|
+
versionFiles(slug, version) {
|
|
291
|
+
const dir = this.#versionDir(slug, version);
|
|
292
|
+
return existsSync(dir) ? readdirSync(dir, { recursive: true }).map(String).sort() : [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
export function localArtifactStub(opts) {
|
|
296
|
+
return new LocalArtifactStub(opts);
|
|
297
|
+
}
|
|
@@ -209,9 +209,10 @@ export interface BashReadonlyRootBoundary {
|
|
|
209
209
|
/** Base for resolving relative operands. Defaults to `roots[0]`. Callers tracking an observable `cd`
|
|
210
210
|
* should pass the LIVE working directory, or a `..` operand will be judged against the wrong base. */
|
|
211
211
|
cwd?: string;
|
|
212
|
-
/** Expansion for a leading
|
|
213
|
-
*
|
|
214
|
-
*
|
|
212
|
+
/** Expansion for a leading `~` — and the value the read faces substitute for the `$HOME` / `${HOME}` word
|
|
213
|
+
* spellings before segmenting ({@link foldHomeVariableSpelling}). Without it a `~`-rooted operand is unresolvable
|
|
214
|
+
* and demotes (with no out-of-root signal — the classifier does not know where it points, which is a
|
|
215
|
+
* different verdict from knowing it points outside). */
|
|
215
216
|
homeDir?: string;
|
|
216
217
|
/** The sensitive-path read deny judge over LEXICALLY RESOLVED operands: a hit
|
|
217
218
|
* demotes the command (ask, never auto-allow), independently of the roots — in-root operands are
|
|
@@ -248,11 +249,13 @@ export interface BashReadonlyRootBoundary {
|
|
|
248
249
|
export interface CompoundReadonlyVerdict {
|
|
249
250
|
/** Demotion reason (the same string {@link classifyCompoundReadonly} returns), or undefined. */
|
|
250
251
|
reason?: string;
|
|
251
|
-
/** Set
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
252
|
+
/** Set when a listed reader's statically resolved path operand lies outside the roots. With `reason` the
|
|
253
|
+
* containment sentence itself, the roots were the SOLE obstacle (every segment a listed reader, no
|
|
254
|
+
* blocking/device/guarded-form arm fired, every operand resolved) and the narrow "allow reading from <dir>"
|
|
255
|
+
* approval would make the command auto-allowable; with `reason` a SHAPE refusal (follow mode, a device, a
|
|
256
|
+
* blocking stdin read, a guarded verb form, multi-cd) or an UNRESOLVABLE-operand refusal (another operand
|
|
257
|
+
* the walk could not read) the finding rides beside the sentence — the read of that path is the same read,
|
|
258
|
+
* so the boundary's objection is stated either way, while the command stays refused after any grant. */
|
|
256
259
|
outOfRootRead?: true;
|
|
257
260
|
/** The resolved out-of-root paths, de-duplicated, in first-seen order. Present iff `outOfRootRead`. */
|
|
258
261
|
outOfRootPaths?: readonly string[];
|
|
@@ -312,6 +315,24 @@ export interface CompoundReadonlyVerdict {
|
|
|
312
315
|
* and miss the other.
|
|
313
316
|
*/
|
|
314
317
|
undecidedPaths?: readonly string[];
|
|
318
|
+
/**
|
|
319
|
+
* Operands (written spellings) the walk could NOT resolve at all — a `~user` form, a `~` under no declared
|
|
320
|
+
* home, an argument the shell rewrites (brace/variable/command expansion), a `cd` target it cannot follow
|
|
321
|
+
* (`cd -`, bare `cd`, a pattern). The STRUCTURAL form of the unresolvable sentence: `reason` carries the
|
|
322
|
+
* first one's sentence, this member every one of them, and both survive whatever other sentence (a shape
|
|
323
|
+
* refusal) takes precedence. CONSUMER CONTRACT: non-empty ⇒ the boundary could not read where the command
|
|
324
|
+
* reads — a fail-closed ask, never a vouch (plain on the classify seat, whose classifier asks beside it; MANDATED on the
|
|
325
|
+
* boundary-only seat, where nothing else would ask).
|
|
326
|
+
*/
|
|
327
|
+
unresolvedOperands?: readonly string[];
|
|
328
|
+
/**
|
|
329
|
+
* The grammar gate refused the WHOLE command before the walk ran (an escape, a substitution, a subshell, a
|
|
330
|
+
* redirection, a line break — see {@link rejectedSansRedirection} and the compound gate): no operand was judged,
|
|
331
|
+
* and what the shell would run is not knowable from the text (a reader can be spelled `ca\t`, fed by `<`, or hidden
|
|
332
|
+
* in `$(…)`). CONSUMER CONTRACT: present ⇒ the boundary judged nothing — MANDATED on both probe seats (a name-reading
|
|
333
|
+
* arm or a stored allow rule must not retire a read nobody judged). Structure, never the sentence.
|
|
334
|
+
*/
|
|
335
|
+
refusedWhole?: true;
|
|
315
336
|
/**
|
|
316
337
|
* Operands of a RECURSIVE/EXPANDING read form (`grep -r`, `ls -R`, `du`, … — see
|
|
317
338
|
* {@link RECURSIVE_READ_FORMS}) judged with a {@link BashReadonlyRootBoundary.denyMatch} seat wired.
|
|
@@ -343,6 +364,58 @@ export interface CompoundReadonlyVerdict {
|
|
|
343
364
|
* uses the host's native separator.
|
|
344
365
|
*/
|
|
345
366
|
export declare function formatOutOfRootReadApprovalOption(directory: string): string;
|
|
367
|
+
/** Resolve one operand against the boundary's base, WITHOUT touching the filesystem. Returns undefined
|
|
368
|
+
* when the token cannot be resolved statically — the fail-closed direction, reported as its own
|
|
369
|
+
* demotion rather than silently treated as in-root.
|
|
370
|
+
*
|
|
371
|
+
* The resolution is TEXTUAL, and its limitation runs in BOTH directions (only the first shows up as
|
|
372
|
+
* a refusal, so only that half tends to get reported):
|
|
373
|
+
* · false REFUSAL — a non-canonical spelling of an in-root path (a symlinked `/tmp` → `/private/tmp`
|
|
374
|
+
* prefix) does not textually match the canonical roots and is refused;
|
|
375
|
+
* · false ALLOW — a symlink INSIDE a root that points outside it (`<root>/vendor` → `/etc`) resolves
|
|
376
|
+
* to a textual prefix match, so `cat vendor/passwd` is judged in-root and the real command follows
|
|
377
|
+
* the link. This face is therefore lexical containment, NOT filesystem containment: it is weaker
|
|
378
|
+
* than the structured Read/Grep tools, which canonicalize through `realpath` before comparing
|
|
379
|
+
* (see safety.ts's `canonicalizeAndEnforceRoot`). Closing it means resolving each candidate against
|
|
380
|
+
* the live filesystem, which this function deliberately cannot do (it is synchronous, pure, and
|
|
381
|
+
* shared with the approval-gate face); the seam for it is the ENFORCING caller, which holds the
|
|
382
|
+
* ExecutionEnv.
|
|
383
|
+
*
|
|
384
|
+
* The verdict now carries {@link CompoundReadonlyVerdict.checkedPaths} (the
|
|
385
|
+
* candidates that landed inside), and `createBashReadonlyTool` canonicalizes them on a LOCAL env
|
|
386
|
+
* before running — closing BOTH directions above for that face (an escaping symlink is refused; a
|
|
387
|
+
* non-canonical spelling that resolves back inside is no longer refused). Two residuals stay here by
|
|
388
|
+
* construction, and belong to the caller rather than to this function: the compound/approval-gate face
|
|
389
|
+
* has no filesystem and stays lexical, and a REMOTE env keeps the lexical behaviour (its
|
|
390
|
+
* `canonicalPath` is an RPC per candidate). */
|
|
391
|
+
export declare function resolveOperandLexically(base: string | undefined, operand: string, homeDir: string | undefined): string | undefined;
|
|
392
|
+
/** Whether a command the grammar gate REFUSED WHOLE carries evidence that it may READ: a listed reader or a shell
|
|
393
|
+
* re-entry program anywhere in it, a substitution where a program name stands (`$C /etc/passwd`), or a stdin
|
|
394
|
+
* redirection from a non-literal target (`cat < $F`). The out-of-root and base-mover readings over its literal path
|
|
395
|
+
* tokens are the containment gate's, asked separately. A refused command with none of these — a write through a
|
|
396
|
+
* redirection (`echo hi > out.txt`), a function definition — names no read for the boundary to mandate: the syntax
|
|
397
|
+
* refusal stays the classify seat's own plain ask. */
|
|
398
|
+
export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>): boolean;
|
|
399
|
+
/**
|
|
400
|
+
* The home directory's VARIABLE spellings, SUBSTITUTED with the declared value before a read face segments the
|
|
401
|
+
* command: `$HOME` / `${HOME}` at the START of a word (an empty quote pair before it included), unquoted or
|
|
402
|
+
* double-quoted, followed by `/` or by the word's end. Those are exactly the words whose expansion is the home
|
|
403
|
+
* directory and nothing else, so the word is rewritten to what bash hands the program — the value itself, in
|
|
404
|
+
* place, with the word's quote structure untouched (`"$HOME/x"` → `"/home/u/x"`, `$HOME"/x"` → `/home/u"/x"`)
|
|
405
|
+
* — and every arm reads an ordinary absolute path; no tilde is minted, so the tilde-prefix rule of
|
|
406
|
+
* {@link tokenizeSegment} judges only tildes the command actually wrote. Substituted only against a DECLARED
|
|
407
|
+
* home (`homeDir`): with none declared the child's `$HOME` may differ from what `~` falls back to, so the
|
|
408
|
+
* spelling is left as written and meets the substitution hard-reject. The value must survive this file's own
|
|
409
|
+
* reading of the rewritten word: the UNQUOTED spelling only for a value with no `$IFS` whitespace, glob or
|
|
410
|
+
* shell metacharacter (word splitting / pathname expansion / a connector this file would then split on — none
|
|
411
|
+
* of which a real expansion is subject to here), the double-quoted one for any value carrying no `"`, `$`,
|
|
412
|
+
* backtick, backslash or line break (the characters that would change the quote structure or meet the
|
|
413
|
+
* hard-reject). Everything else is left as written: a single-quoted spelling does not expand, and a mid-word
|
|
414
|
+
* or suffixed one (`x$HOME`, `$HOME.bak`, `$HOMES`) still meets the substitution hard-reject. Bails whole on
|
|
415
|
+
* an escape or a backtick in the command (the hard-reject refuses those strings regardless, and the quote
|
|
416
|
+
* walk below is exact only without them) and on unbalanced quoting.
|
|
417
|
+
*/
|
|
418
|
+
export declare function foldHomeVariableSpelling(command: string, homeDir: string | undefined): string;
|
|
346
419
|
/**
|
|
347
420
|
* {@link classifyCompoundReadonly} with the structured verdict. The name/operator/blocking arms
|
|
348
421
|
* run FIRST and unchanged, so an existing demotion keeps its exact reason and never acquires the
|
|
@@ -568,3 +641,12 @@ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
|
|
|
568
641
|
* ({@link NON_ASCII_WHITESPACE}), so the grammar's tokenization and bash's stay identical.
|
|
569
642
|
*/
|
|
570
643
|
export declare function classifyBoundedReadonlyPollLoop(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
|
|
644
|
+
/**
|
|
645
|
+
* The poll-loop face's STRUCTURED verdict — the same {@link CompoundReadonlyVerdict} the compound face
|
|
646
|
+
* returns, so a consumer reads the boundary's structural findings (deny row, containment, recursive or
|
|
647
|
+
* undecided operands) off the loop body exactly as off a plain compound, beside whatever sentence the
|
|
648
|
+
* loop grammar or the body's shape produced. {@link classifyBoundedReadonlyPollLoop} is its `reason`
|
|
649
|
+
* projection. A refusal of the LOOP GRAMMAR itself (not a bounded loop, an unlisted body program, an
|
|
650
|
+
* unvetted sleep) carries no structure: no reader segment was walked.
|
|
651
|
+
*/
|
|
652
|
+
export declare function classifyBoundedReadonlyPollLoopDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
|