@uchibeke/gallivant 0.0.0-stage → 0.2.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/CONTRIBUTING.md +26 -0
- package/LICENSE +204 -0
- package/README.md +239 -2
- package/SECURITY.md +19 -0
- package/bin/gallivant.mjs +261 -0
- package/bin/package-plugin.mjs +16 -0
- package/bin/release-live.mjs +29 -0
- package/bin/release-package.mjs +23 -0
- package/bin/session-runtime.mjs +13 -0
- package/bin/task-delivery-state.mjs +12 -0
- package/docs/README.md +72 -0
- package/docs/agent-setup.md +228 -0
- package/docs/app.md +111 -0
- package/docs/architecture.md +265 -0
- package/docs/chains.md +102 -0
- package/docs/codex-cloud.md +284 -0
- package/docs/commands.md +649 -0
- package/docs/config.md +437 -0
- package/docs/execution.md +796 -0
- package/docs/guardrails.md +311 -0
- package/docs/install.md +398 -0
- package/docs/launch.md +45 -0
- package/docs/learning.md +369 -0
- package/docs/network-recovery.md +101 -0
- package/docs/openai-agents.md +457 -0
- package/docs/prd.md +246 -0
- package/docs/preparation.md +110 -0
- package/docs/release-qualification.md +196 -0
- package/docs/releasing.md +149 -0
- package/docs/runtime-environment.md +113 -0
- package/docs/task-delivery.md +476 -0
- package/docs/tasks.md +739 -0
- package/lib/aport.mjs +95 -0
- package/lib/attribution.mjs +95 -0
- package/lib/block.mjs +89 -0
- package/lib/capabilities.mjs +3 -0
- package/lib/chains.mjs +222 -0
- package/lib/codex_cloud.mjs +160 -0
- package/lib/codex_cloud_config.mjs +20 -0
- package/lib/codex_cloud_input.mjs +49 -0
- package/lib/codex_cloud_result.mjs +74 -0
- package/lib/commands/doctor.mjs +466 -0
- package/lib/commands/init.mjs +245 -0
- package/lib/commands/learn.mjs +54 -0
- package/lib/commands/new.mjs +47 -0
- package/lib/commands/ready.mjs +45 -0
- package/lib/commands/register.mjs +249 -0
- package/lib/commands/resume.mjs +224 -0
- package/lib/commands/serve.mjs +349 -0
- package/lib/commands/start.mjs +247 -0
- package/lib/commands/start_services.mjs +22 -0
- package/lib/commands/status.mjs +150 -0
- package/lib/commands/task.mjs +73 -0
- package/lib/commands/up.mjs +228 -0
- package/lib/commands/why.mjs +143 -0
- package/lib/config.mjs +461 -0
- package/lib/context.mjs +102 -0
- package/lib/control.mjs +202 -0
- package/lib/control_contract.mjs +16 -0
- package/lib/coordinator.mjs +19 -0
- package/lib/credential.mjs +161 -0
- package/lib/daemon.mjs +156 -0
- package/lib/deliverable.mjs +357 -0
- package/lib/events.mjs +261 -0
- package/lib/executable.mjs +31 -0
- package/lib/execution.mjs +707 -0
- package/lib/execution_contract.mjs +15 -0
- package/lib/execution_input.mjs +4 -0
- package/lib/execution_io.mjs +94 -0
- package/lib/execution_journal.mjs +69 -0
- package/lib/execution_preparation.mjs +29 -0
- package/lib/execution_result.mjs +266 -0
- package/lib/fs.mjs +140 -0
- package/lib/gh.mjs +289 -0
- package/lib/git.mjs +434 -0
- package/lib/harness.mjs +521 -0
- package/lib/harness_log.mjs +432 -0
- package/lib/hash.mjs +16 -0
- package/lib/identity.mjs +98 -0
- package/lib/learning.mjs +654 -0
- package/lib/learning_contract.mjs +2 -0
- package/lib/learning_data.mjs +135 -0
- package/lib/learning_input.mjs +111 -0
- package/lib/learning_qualification.mjs +67 -0
- package/lib/learning_signer.mjs +151 -0
- package/lib/learning_store.mjs +88 -0
- package/lib/log.mjs +53 -0
- package/lib/network_diagnostics.mjs +64 -0
- package/lib/network_recovery.mjs +42 -0
- package/lib/notify.mjs +58 -0
- package/lib/openai_agents.mjs +373 -0
- package/lib/openai_agents_config.mjs +14 -0
- package/lib/openai_agents_http.mjs +87 -0
- package/lib/openai_agents_input.mjs +15 -0
- package/lib/openai_agents_sources.mjs +112 -0
- package/lib/owner_delivery.mjs +443 -0
- package/lib/plugin_package.mjs +340 -0
- package/lib/prd.mjs +635 -0
- package/lib/preparation_agents.mjs +33 -0
- package/lib/preparation_codex.mjs +212 -0
- package/lib/probe.mjs +126 -0
- package/lib/prompt.mjs +238 -0
- package/lib/provider_health.mjs +55 -0
- package/lib/public-format.mjs +31 -0
- package/lib/public_output.mjs +49 -0
- package/lib/release_package.mjs +154 -0
- package/lib/repository_files.mjs +86 -0
- package/lib/run.mjs +385 -0
- package/lib/run_log.mjs +94 -0
- package/lib/runtime-environment.mjs +87 -0
- package/lib/runtime_dependencies.mjs +15 -0
- package/lib/runtime_node.mjs +107 -0
- package/lib/runtime_state.mjs +174 -0
- package/lib/sources/index.mjs +27 -0
- package/lib/sources/prd.mjs +50 -0
- package/lib/start_contract.mjs +37 -0
- package/lib/state.mjs +246 -0
- package/lib/task_allowance.mjs +31 -0
- package/lib/task_completion.mjs +61 -0
- package/lib/task_contract.mjs +450 -0
- package/lib/task_delivery.mjs +219 -0
- package/lib/task_delivery_cancel.mjs +34 -0
- package/lib/task_delivery_contract.mjs +61 -0
- package/lib/task_delivery_mcp.mjs +40 -0
- package/lib/task_delivery_store.mjs +43 -0
- package/lib/task_delivery_view.mjs +88 -0
- package/lib/task_environment.mjs +281 -0
- package/lib/task_prepare.mjs +224 -0
- package/lib/task_store.mjs +38 -0
- package/lib/tasks.mjs +513 -0
- package/lib/tick.mjs +1654 -0
- package/lib/time.mjs +36 -0
- package/lib/tracker/assets.mjs +43 -0
- package/lib/tracker/pages.mjs +320 -0
- package/lib/tracker/runs.mjs +102 -0
- package/lib/tracker/stream.mjs +74 -0
- package/lib/tracker_process.mjs +107 -0
- package/lib/verification_sandbox.mjs +181 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +605 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +156 -0
- package/node_modules/smol-toml/dist/error.d.ts +37 -0
- package/node_modules/smol-toml/dist/error.js +67 -0
- package/node_modules/smol-toml/dist/extract.js +289 -0
- package/node_modules/smol-toml/dist/index.cjs +770 -0
- package/node_modules/smol-toml/dist/index.d.ts +44 -0
- package/node_modules/smol-toml/dist/index.js +34 -0
- package/node_modules/smol-toml/dist/parse.d.ts +73 -0
- package/node_modules/smol-toml/dist/parse.js +164 -0
- package/node_modules/smol-toml/dist/primitive.js +151 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
- package/node_modules/smol-toml/dist/stringify.js +219 -0
- package/node_modules/smol-toml/dist/struct.js +156 -0
- package/node_modules/smol-toml/dist/util.d.ts +43 -0
- package/node_modules/smol-toml/dist/util.js +57 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/npm-shrinkwrap.json +1823 -0
- package/package.json +105 -4
- package/templates/claude-settings.json +18 -0
- package/templates/passport.gallivant.json +83 -0
- package/templates/plugin/bundled-runtime.json +26 -0
- package/templates/plugin/schemas/mcp.schema.json +120 -0
- package/templates/plugin/schemas/plugin.schema.json +65 -0
- package/templates/plugin/task-delivery.SKILL.md +60 -0
- package/templates/prd.md +32 -0
- package/templates/tracker/tracker.css +301 -0
- package/templates/tracker/tracker.js +721 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Direct documented Agents REST transport. No SDK loop or inference fallback.
|
|
2
|
+
import { executionError, EXECUTION_LIMITS } from "./execution_io.mjs";
|
|
3
|
+
export const AGENTS_API_VERSION = "agents=v1";
|
|
4
|
+
export const AGENTS_LIMITS = Object.freeze({ responseBytes: 1_048_576, pageSize: 100, pages: 20, streamBytes: 65_536, streamEvents: 100, uploadBytes: 5 * 1024 * 1024, inputBytes: 3 * 1024 * 1024 });
|
|
5
|
+
function retryDelay(value, now) {
|
|
6
|
+
const header = value?.trim() ?? "";
|
|
7
|
+
// HTTP accepts delta-seconds and three HTTP-date formats. Do not let
|
|
8
|
+
// Date.parse interpret malformed numeric values as calendar dates.
|
|
9
|
+
const date = /^(?:[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT|[A-Za-z]+, \d{2}-[A-Za-z]{3}-\d{2} \d{2}:\d{2}:\d{2} GMT|[A-Za-z]{3} [A-Za-z]{3} [ \d]\d \d{2}:\d{2}:\d{2} \d{4})$/;
|
|
10
|
+
const delay = /^\d+$/.test(header) ? Math.min(EXECUTION_LIMITS.maxBackoffMs, Number(header) * 1000) : date.test(header) ? Date.parse(header.endsWith(" GMT") ? header : `${header} GMT`) - now : 0;
|
|
11
|
+
return Number.isFinite(delay) ? Math.min(EXECUTION_LIMITS.maxBackoffMs, Math.max(0, delay)) : 0;
|
|
12
|
+
}
|
|
13
|
+
export function agentsHttp({ credential, organization, project, fetcher = fetch, clock = Date }) {
|
|
14
|
+
const headers = async () => {
|
|
15
|
+
const key = await credential();
|
|
16
|
+
if (!key) throw executionError("AGENTS_AUTH", "provide an application OPENAI_API_KEY with api.agents.read/write and api.responses.write; API billing is separate from ChatGPT subscriptions");
|
|
17
|
+
return { Authorization: `Bearer ${key}`, "OpenAI-Beta": AGENTS_API_VERSION, ...(organization ? { "OpenAI-Organization": organization } : {}), ...(project ? { "OpenAI-Project": project } : {}) };
|
|
18
|
+
};
|
|
19
|
+
async function response(path, { signal, method = "GET", body, key, accept } = {}) {
|
|
20
|
+
if (!/^\/(?:agents|models)\//.test(path) && path !== "/agents/sessions") throw executionError("AGENTS_PATH", "unsupported Agents endpoint");
|
|
21
|
+
if (body && Buffer.byteLength(JSON.stringify(body)) > AGENTS_LIMITS.inputBytes) throw executionError("AGENTS_SIZE", "input exceeds the bounded request; supply smaller portable sources");
|
|
22
|
+
const result = await fetcher(`https://api.openai.com/v1${path}`, { method, signal, redirect: "error", headers: { ...await headers(), ...(body ? { "Content-Type": "application/json" } : {}), ...(key ? { "Idempotency-Key": key } : {}), ...(accept ? { Accept: accept } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
23
|
+
if (!result.ok) {
|
|
24
|
+
await result.body?.cancel(); // Never put provider error bodies/credentials in public errors.
|
|
25
|
+
const error = executionError(result.status === 401 || result.status === 403 ? "AGENTS_AUTH" : result.status === 429 ? "AGENTS_THROTTLED" : "AGENTS_HTTP", `Agents API HTTP ${result.status}; request ${result.headers.get("x-request-id") || "unknown"}; check scoped access, billing or resource configuration`);
|
|
26
|
+
error.retryAfterMs = retryDelay(result.headers.get("retry-after"), clock.now());
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
async function bytes(result, max = AGENTS_LIMITS.responseBytes) {
|
|
32
|
+
if (Number(result.headers.get("content-length") || 0) > max) { await result.body?.cancel(); throw executionError("AGENTS_SIZE", "provider response exceeds the bounded limit"); }
|
|
33
|
+
const reader = result.body?.getReader();
|
|
34
|
+
if (!reader) return Buffer.alloc(0);
|
|
35
|
+
const chunks = []; let size = 0;
|
|
36
|
+
try {
|
|
37
|
+
for (;;) { const { value, done } = await reader.read(); if (done) break; size += value.length; if (size > max) throw executionError("AGENTS_SIZE", "provider response exceeds the bounded limit"); chunks.push(value); }
|
|
38
|
+
return Buffer.concat(chunks);
|
|
39
|
+
} finally { await reader.cancel(); reader.releaseLock(); }
|
|
40
|
+
}
|
|
41
|
+
const request = async (path, options) => {
|
|
42
|
+
const value = await bytes(await response(path, options));
|
|
43
|
+
if (!value.length) return null;
|
|
44
|
+
try { return JSON.parse(value.toString("utf8")); } catch { throw executionError("AGENTS_PROTOCOL", "invalid provider JSON"); }
|
|
45
|
+
};
|
|
46
|
+
// Connect first, buffer while the caller retrieves authoritative state. Each
|
|
47
|
+
// bounded operation closes its stream; periodic coordinator polling recovers it.
|
|
48
|
+
async function subscribe(sessionId, signal) {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const abort = () => controller.abort(); signal?.addEventListener("abort", abort, { once: true });
|
|
51
|
+
if (signal?.aborted) controller.abort();
|
|
52
|
+
let reader;
|
|
53
|
+
try { reader = (await response(`/agents/sessions/${encodeURIComponent(sessionId)}/events?stream=true`, { signal: controller.signal, accept: "text/event-stream" })).body.getReader(); }
|
|
54
|
+
catch (error) { signal?.removeEventListener("abort", abort); throw error; }
|
|
55
|
+
const events = []; let gap = false, pending = "", total = 0;
|
|
56
|
+
const decoder = new TextDecoder();
|
|
57
|
+
const pumping = (async () => {
|
|
58
|
+
try {
|
|
59
|
+
for (;;) {
|
|
60
|
+
const { done, value } = await reader.read(); if (done) { gap = true; break; }
|
|
61
|
+
total += value.length;
|
|
62
|
+
if (total > AGENTS_LIMITS.streamBytes) { gap = true; break; }
|
|
63
|
+
pending = (pending + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
|
|
64
|
+
let end;
|
|
65
|
+
while ((end = pending.indexOf("\n\n")) >= 0) {
|
|
66
|
+
const block = pending.slice(0, end); pending = pending.slice(end + 2);
|
|
67
|
+
const data = block.split("\n").filter(line => line.startsWith("data:")).map(line => line.slice(5).trimStart()).join("\n");
|
|
68
|
+
if (!data || data === "[DONE]") continue;
|
|
69
|
+
try { const event = JSON.parse(data); if (events.length < AGENTS_LIMITS.streamEvents) events.push(event); else gap = true; }
|
|
70
|
+
catch { gap = true; }
|
|
71
|
+
}
|
|
72
|
+
if (events.length >= AGENTS_LIMITS.streamEvents) { gap = true; break; }
|
|
73
|
+
}
|
|
74
|
+
} catch { gap = true; }
|
|
75
|
+
})();
|
|
76
|
+
return { events, get gap() { return gap; }, async close() { controller.abort(); await reader.cancel().catch(() => {}); await pumping; reader.releaseLock(); signal?.removeEventListener("abort", abort); } };
|
|
77
|
+
}
|
|
78
|
+
async function page(path, signal, after = null) {
|
|
79
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
80
|
+
const value = await request(`${path}${separator}limit=${AGENTS_LIMITS.pageSize}&order=asc${after ? `&after=${encodeURIComponent(after)}` : ""}`, { signal });
|
|
81
|
+
if (!Array.isArray(value?.data) || value.data.length > AGENTS_LIMITS.pageSize || typeof value.has_more !== "boolean") throw executionError("AGENTS_PROTOCOL", "invalid provider page");
|
|
82
|
+
const next = value.has_more ? value.last_id || value.data.at(-1)?.id : null;
|
|
83
|
+
if (value.has_more && (!next || next === after || typeof next !== "string" || next.length > 512)) throw executionError("AGENTS_PROTOCOL", "provider pagination does not advance");
|
|
84
|
+
return { data: value.data, next };
|
|
85
|
+
}
|
|
86
|
+
return { request, subscribe, page, download: async (path, signal) => bytes(await response(path, { signal })) };
|
|
87
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Same source/authority boundary for first launch, resume and review corrections.
|
|
2
|
+
import { prepareAgentsSources } from "./openai_agents_sources.mjs";
|
|
3
|
+
import { executionError } from "./execution_io.mjs";
|
|
4
|
+
import { taskProviderAllowance } from "./task_allowance.mjs";
|
|
5
|
+
export async function agentsInput(ctx, id, entry, input, contextPaths) {
|
|
6
|
+
if (contextPaths?.length) throw executionError("EXECUTION_POLICY", "hosted backend cannot enforce configured read-only private context_paths");
|
|
7
|
+
const config = ctx.config.harness[entry.harness];
|
|
8
|
+
if (!config) throw executionError("AGENTS_CONFIG", "selected managed API account is not configured");
|
|
9
|
+
const overlays = [];
|
|
10
|
+
if (entry.prd) overlays.push({ path: entry.prd, text: ctx.readPrd(id) });
|
|
11
|
+
for (const path of entry.sources || []) overlays.push({ path, text: ctx.readSource(id, path) });
|
|
12
|
+
const source = await prepareAgentsSources({ worktree: entry.worktree, inputHead: input.inputHead, repo: entry.repo, branch: entry.branch, base: entry.base, overlays });
|
|
13
|
+
return { ...input, repo: entry.repo, branch: entry.branch, base: entry.base, allowance: taskProviderAllowance(config.allowance, entry.task), mandatoryPolicy: config.mandatory_policy || [], model: config.model, environment: config.environment,
|
|
14
|
+
sources: { data: source.bytes.toString("base64"), hash: source.hash, hashes: source.hashes, modes: source.modes, inputHead: source.inputHead, repo: source.repo, branch: source.branch } };
|
|
15
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { gzipSync } from "node:zlib";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const sourceExec = promisify(execFile);
|
|
6
|
+
import { readRepositoryText, assertRepositoryTargets } from "./repository_files.mjs";
|
|
7
|
+
import { parseTaskSources } from "./task_contract.mjs";
|
|
8
|
+
import { executionError } from "./execution_io.mjs";
|
|
9
|
+
import { AGENTS_LIMITS } from "./openai_agents_http.mjs";
|
|
10
|
+
export const contentHash = value => value === null ? null : createHash("sha256").update(value).digest("hex");
|
|
11
|
+
export function portablePath(path) {
|
|
12
|
+
const parsed = parseTaskSources([path]);
|
|
13
|
+
if (parsed.problems.length || parsed.paths[0] !== path || path.split("/").some(p => !p || p === ".") || /[\x00-\x1f\x7f]/.test(path)) throw executionError("AGENTS_SOURCE", "unsafe or private portable source/result path");
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
// Only approved brief/source overlays may differ from HEAD. An explicit resume
|
|
17
|
+
// must never describe preserved local edits while dispatching stale Git blobs.
|
|
18
|
+
export async function assertAgentsCheckout({ worktree, overlays = [], signal }, exec = sourceExec) {
|
|
19
|
+
const allowed = new Set(overlays.map(item => portablePath(typeof item === "string" ? item : item.path)));
|
|
20
|
+
const { stdout } = await exec("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: worktree, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }, signal, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, timeout: 20_000 });
|
|
21
|
+
for (const entry of stdout.split("\0").filter(Boolean)) {
|
|
22
|
+
if (entry.length < 4 || entry[2] !== " " || /[RCU]/.test(entry.slice(0, 2)) || !allowed.has(entry.slice(3))) {
|
|
23
|
+
throw executionError("AGENTS_DIRTY_SOURCE", "hosted execution requires unfinished repository edits to be committed before resume; preserve the index/worktree, commit the intended changes on the owned branch, then resume the existing session; only approved brief/source overlays may remain uncommitted");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Immutable Git blobs, plus the foundation's explicitly snapshotted brief and
|
|
28
|
+
// carried sources. Never upload the checkout's environment or Git history.
|
|
29
|
+
export async function prepareAgentsSources({ worktree, inputHead, repo, branch, base, overlays = [], signal }, exec = sourceExec) {
|
|
30
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(inputHead || "") || !/^[\w.-]+\/[\w.-]+$/.test(repo || "") || typeof branch !== "string" || branch === base) throw executionError("AGENTS_SOURCE", "exact source head and owned repository/branch/base are required");
|
|
31
|
+
const options = { cwd: worktree, signal, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, timeout: 20_000 };
|
|
32
|
+
const { stdout: head } = await exec("git", ["rev-parse", "HEAD"], options);
|
|
33
|
+
if (head.trim() !== inputHead) throw executionError("AGENTS_SOURCE", "source head changed during preparation");
|
|
34
|
+
await assertAgentsCheckout({ worktree, overlays, signal }, exec);
|
|
35
|
+
const { stdout: listing } = await exec("git", ["ls-tree", "-r", "-z", inputHead], options);
|
|
36
|
+
const files = new Map(); let size = 0;
|
|
37
|
+
const treeEntry = new RegExp(`^(100644|100755) blob ([a-f0-9]{${inputHead.length}})\\t(.+)$`, "s");
|
|
38
|
+
for (const entry of listing.split("\0").filter(Boolean)) {
|
|
39
|
+
const match = entry.match(treeEntry);
|
|
40
|
+
if (!match) throw executionError("AGENTS_SOURCE", "portable source includes a symlink, submodule or unsupported mode");
|
|
41
|
+
const [, mode, blob, rawPath] = match, path = portablePath(rawPath);
|
|
42
|
+
const { stdout: data } = await exec("git", ["cat-file", "blob", blob], { ...options, encoding: "buffer" });
|
|
43
|
+
size += data.length;
|
|
44
|
+
if (size > 10 * 1024 * 1024) throw executionError("AGENTS_SIZE", "portable source exceeds the 10 MiB expanded limit");
|
|
45
|
+
files.set(path, { path, mode: Number.parseInt(mode.slice(-3), 8), data: data.toString("base64"), hash: contentHash(data) });
|
|
46
|
+
}
|
|
47
|
+
for (const item of overlays) {
|
|
48
|
+
const path = portablePath(item.path);
|
|
49
|
+
if (typeof item.text !== "string" || readRepositoryText(worktree, path) !== item.text) throw executionError("AGENTS_SOURCE", "portable brief/source differs from its saved snapshot");
|
|
50
|
+
const bytes = Buffer.from(item.text); size += bytes.length;
|
|
51
|
+
if (size > 10 * 1024 * 1024) throw executionError("AGENTS_SIZE", "portable source exceeds the expanded limit");
|
|
52
|
+
files.set(path, { path, mode: files.get(path)?.mode ?? 0o644, data: bytes.toString("base64"), hash: contentHash(bytes) });
|
|
53
|
+
}
|
|
54
|
+
assertRepositoryTargets([...files.keys()], [...files.keys()]);
|
|
55
|
+
await assertAgentsCheckout({ worktree, overlays, signal }, exec);
|
|
56
|
+
if ((await exec("git", ["rev-parse", "HEAD"], options)).stdout.trim() !== inputHead) throw executionError("AGENTS_SOURCE", "source head changed during preparation");
|
|
57
|
+
// Carried files can be uncommitted on the coordinator's fresh checkout. Include
|
|
58
|
+
// them in delivery even when the agent leaves their approved bytes unchanged,
|
|
59
|
+
// so the single PR writer commits them with the implementation. Later receipt
|
|
60
|
+
// repairs use the same verified files without manufacturing a code change.
|
|
61
|
+
const manifest = { version: 1, inputHead, repo, branch, base, deliveryPaths: overlays.map(item => item.path), files: [...files.values()] };
|
|
62
|
+
const bytes = gzipSync(Buffer.from(JSON.stringify(manifest)));
|
|
63
|
+
if (bytes.length > AGENTS_LIMITS.uploadBytes || bytes.length * 4 / 3 > AGENTS_LIMITS.inputBytes - 65_536) throw executionError("AGENTS_SIZE", "portable snapshot exceeds inline request limits; reduce explicitly selected context");
|
|
64
|
+
return { bytes, hash: contentHash(bytes), inputHead, repo, branch, base, hashes: Object.fromEntries(manifest.files.map(f => [f.path, f.hash])), modes: Object.fromEntries(manifest.files.map(f => [f.path, f.mode])) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Shipped into the sandbox. Outputs contain text changes and test records only;
|
|
68
|
+
// GitHub credentials and API keys stay with the coordinator's existing PR writer.
|
|
69
|
+
export const AGENTS_WORKSPACE_HELPER = String.raw`import base64,gzip,hashlib,json,pathlib,shutil,subprocess,sys
|
|
70
|
+
root=pathlib.Path('/workspace/repo')
|
|
71
|
+
def safe(path):
|
|
72
|
+
p=pathlib.PurePosixPath(path)
|
|
73
|
+
if p.is_absolute() or not p.parts or any(x in ('..','.git') for x in p.parts): raise ValueError('unsafe path')
|
|
74
|
+
target=root.joinpath(*p.parts)
|
|
75
|
+
if any(x.is_symlink() for x in (target,*target.parents) if x==root or root in x.parents): raise ValueError('output symlink')
|
|
76
|
+
return target
|
|
77
|
+
def sha(data): return hashlib.sha256(data).hexdigest()
|
|
78
|
+
def restore(source, expected):
|
|
79
|
+
raw=pathlib.Path(source).read_bytes()
|
|
80
|
+
if sha(raw)!=expected: raise ValueError('snapshot hash mismatch')
|
|
81
|
+
m=json.loads(gzip.decompress(raw))
|
|
82
|
+
if root.exists(): shutil.rmtree(root)
|
|
83
|
+
root.mkdir()
|
|
84
|
+
for f in m['files']:
|
|
85
|
+
p=safe(f['path']); data=base64.b64decode(f['data'],validate=True)
|
|
86
|
+
if sha(data)!=f['hash']: raise ValueError('source hash mismatch')
|
|
87
|
+
p.parent.mkdir(parents=True,exist_ok=True); p.write_bytes(data); p.chmod(f['mode'])
|
|
88
|
+
subprocess.run(['git','init','-b',m['branch']],cwd=root,check=True,stdout=subprocess.DEVNULL)
|
|
89
|
+
subprocess.run(['git','add','--all'],cwd=root,check=True)
|
|
90
|
+
subprocess.run(['git','-c','user.name=Workspace','-c','user.email=workspace@localhost','commit','-qm','Input snapshot'],cwd=root,check=True)
|
|
91
|
+
pathlib.Path('/workspace/baseline.json').write_text(json.dumps(m))
|
|
92
|
+
def publish(prfile, testsfile):
|
|
93
|
+
m=json.loads(pathlib.Path('/workspace/baseline.json').read_text()); before={f['path']:f for f in m['files']}; files=[]
|
|
94
|
+
paths=set(before)
|
|
95
|
+
selected=subprocess.run(['git','ls-files','--cached','--others','--exclude-standard','-z'],cwd=root,check=True,stdout=subprocess.PIPE).stdout.decode('utf-8')
|
|
96
|
+
paths.update(x for x in selected.split('\x00') if x)
|
|
97
|
+
for name in sorted(paths):
|
|
98
|
+
p=safe(name); previous=before.get(name)
|
|
99
|
+
if p.exists() and (p.stat().st_mode&0o777 != (previous['mode'] if previous else 0o644) or p.stat().st_size>max(1000000,len(base64.b64decode(previous['data'])) if previous else 0)): raise ValueError('unsupported output mode/size')
|
|
100
|
+
data=p.read_bytes() if p.exists() else None
|
|
101
|
+
after=sha(data) if data is not None else None
|
|
102
|
+
if after==(previous['hash'] if previous else None) and name not in m.get('deliveryPaths',[]): continue
|
|
103
|
+
if data is not None and b'\x00' in data: raise ValueError('unsupported output content')
|
|
104
|
+
files.append({'path':name,'beforeHash':previous['hash'] if previous else None,'afterHash':after,'text':data.decode('utf-8') if data is not None else None})
|
|
105
|
+
result={'version':1,'inputHead':m['inputHead'],'repo':m['repo'],'branch':m['branch'],'files':files,'pr':json.loads(pathlib.Path(prfile).read_text()),'tests':json.loads(pathlib.Path(testsfile).read_text())}
|
|
106
|
+
raw=json.dumps(result).encode()
|
|
107
|
+
if len(files)>100 or len(raw)>1000000: raise ValueError('bounded result exceeded')
|
|
108
|
+
out=pathlib.Path('/workspace/outputs'); out.mkdir(exist_ok=True); (out/'result.json').write_bytes(raw)
|
|
109
|
+
if sys.argv[1]=='restore': restore(sys.argv[2],sys.argv[3])
|
|
110
|
+
elif sys.argv[1]=='publish': publish(sys.argv[2],sys.argv[3])
|
|
111
|
+
else: raise ValueError('unknown operation')
|
|
112
|
+
`;
|