@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,160 @@
|
|
|
1
|
+
// Supported CLI only. There is no private Cloud HTTP client or review engine.
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execFileAsync, repositoryMatches } from "./gh.mjs";
|
|
4
|
+
import { ensureDir, readJson, writeJsonAtomic } from "./fs.mjs";
|
|
5
|
+
import { executionError, dispatchNotSentError, redact, accessProbeFailure } from "./execution_io.mjs";
|
|
6
|
+
import { validateCloudConfig, CLOUD_VERSION, CLOUD_DISPATCH_UNAVAILABLE } from "./codex_cloud_config.mjs";
|
|
7
|
+
import { contentHash } from "./openai_agents_sources.mjs";
|
|
8
|
+
import { cloudDiffResult, CLOUD_RESULT_PATH } from "./codex_cloud_result.mjs";
|
|
9
|
+
import { requireVerificationSandbox } from "./execution_result.mjs";
|
|
10
|
+
const taskIdFor = value => {
|
|
11
|
+
if (typeof value !== "string" || !/^task_[a-zA-Z0-9_-]{1,200}$/.test(value)) throw executionError("CLOUD_PROTOCOL", "unsupported Cloud task ID; preserve acknowledgement for operator reconciliation");
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
const taskUrl = id => `https://chatgpt.com/codex/tasks/${taskIdFor(id)}`;
|
|
15
|
+
const stateFor = status => ({ pending: "queued", ready: "succeeded", applied: "succeeded", error: "failed" })[status];
|
|
16
|
+
export function cloudCli(config, exec = execFileAsync) {
|
|
17
|
+
return async (args, signal) => {
|
|
18
|
+
// The shared request signal owns the deadline, including submission's
|
|
19
|
+
// acknowledgement window. Keep a bound only for standalone diagnostic calls.
|
|
20
|
+
const { stdout, stderr } = await exec(config.bin, args, { signal, timeout: signal ? 0 : 10_000, maxBuffer: 1_048_576,
|
|
21
|
+
// No GitHub/API credentials or model overrides travel to the CLI. Its own
|
|
22
|
+
// operator sign-in store stays native, outside generated source/prompts.
|
|
23
|
+
env: Object.fromEntries(["PATH", "HOME", "CODEX_HOME", "LANG", "LC_ALL", "TMPDIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME"].filter(key => typeof process.env[key] === "string").map(key => [key, process.env[key]])) });
|
|
24
|
+
return args[0] === "login" ? `${stdout}${stderr || ""}` : stdout;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function listPage(cli, config, signal, cursor = null) {
|
|
28
|
+
const args = ["cloud", "list", "--json", "--env", config.environment, "--limit", "20"];
|
|
29
|
+
if (cursor !== null) args.push("--cursor", cursor);
|
|
30
|
+
let page;
|
|
31
|
+
try { page = JSON.parse(await cli(args, signal)); } catch (error) { if (signal?.aborted) throw error; throw executionError("CLOUD_LIST", "Cloud listing failed; check the authorized account/environment using the configured CLI"); }
|
|
32
|
+
if (!Array.isArray(page.tasks) || page.tasks.length > 20 || page.cursor != null && (typeof page.cursor !== "string" || !page.cursor || page.cursor.length > 4096)) throw executionError("CLOUD_PROTOCOL", "invalid Cloud listing page/cursor");
|
|
33
|
+
return page;
|
|
34
|
+
}
|
|
35
|
+
async function checkCloudVersion(cli, signal) {
|
|
36
|
+
// Experimental output formats are pinned, rather than assuming a future
|
|
37
|
+
// CLI has the same semantics. Recheck every operation, including recovery:
|
|
38
|
+
// the configured executable may have changed since acknowledgement.
|
|
39
|
+
const version = (await cli(["--version"], signal)).trim();
|
|
40
|
+
if (version !== `codex-cli ${CLOUD_VERSION}`) throw executionError("CLOUD_VERSION", `install the qualified Codex ${CLOUD_VERSION} at the configured bin; requalify before changing versions`);
|
|
41
|
+
return version;
|
|
42
|
+
}
|
|
43
|
+
export async function checkCloudAccess(config, { signal, cli = null } = {}) {
|
|
44
|
+
try {
|
|
45
|
+
validateCloudConfig(config); signal ||= AbortSignal.timeout(10_000); cli ||= cloudCli(config);
|
|
46
|
+
const version = await checkCloudVersion(cli, signal);
|
|
47
|
+
if (!(await cli(["login", "status"], signal)).includes("Logged in using ChatGPT")) throw executionError("CLOUD_AUTH", "sign in with the authorized operator's ChatGPT account using the configured Codex login; API-key login is unsupported");
|
|
48
|
+
for (const [command, flags] of [["exec", ["--env", "--branch", "--attempts"]], ["list", ["--env", "--json", "--cursor", "--limit"]], ["diff", ["--attempt"]]]) {
|
|
49
|
+
const help = await cli(["cloud", command, "--help"], signal);
|
|
50
|
+
if (flags.some(flag => !help.includes(flag))) throw executionError("CLOUD_VERSION", "required documented Cloud command capability missing");
|
|
51
|
+
}
|
|
52
|
+
await listPage(cli, config, signal);
|
|
53
|
+
signal.throwIfAborted();
|
|
54
|
+
// Listing proves read access to --env, never its connected repository. The
|
|
55
|
+
// pinned documented CLI has no mapping query, and exec accepts no repo.
|
|
56
|
+
// Keep diagnostics and owned-task recovery, but do not authorize paid work.
|
|
57
|
+
return { ok: false, detail: CLOUD_DISPATCH_UNAVAILABLE.detail, fix: CLOUD_DISPATCH_UNAVAILABLE.fix,
|
|
58
|
+
diagnostics: `${version}; ChatGPT sign-in and selected environment listing authorized; repository mapping and inference unverified`,
|
|
59
|
+
effective: { account: config.account, environment: config.environment, repository: null, continuation: "successor-task", costSource: config.billing_source, hardSpendCap: false, nativeCancel: false, providerAttemptId: null } };
|
|
60
|
+
} catch (error) { return accessProbeFailure(error,signal,"CLOUD_ACCESS",typeof error.code==='string'?redact(error.message):"Check the configured executable, authorized ChatGPT login and published Cloud environment; see docs/codex-cloud.md"); }
|
|
61
|
+
}
|
|
62
|
+
export function makeCloudTransport({ root, taskId, account, config, cli = cloudCli(config), clock = Date, verification = requireVerificationSandbox, verifyRemote = null, checkAccess = checkCloudAccess }) {
|
|
63
|
+
const path = join(root, "cloud.json");
|
|
64
|
+
const read = () => {
|
|
65
|
+
const s = readJson(path, { version: 1, taskId, account, environment: config.environment, repository: config.repository, operations: {} });
|
|
66
|
+
if (s.version !== 1 || s.taskId !== taskId || s.account !== account || s.environment !== config.environment || !repositoryMatches(s.repository, config.repository)) throw executionError("EXECUTION_OWNERSHIP", "Cloud task/account/environment ownership changed; restore recorded configuration");
|
|
67
|
+
return s;
|
|
68
|
+
};
|
|
69
|
+
const save = s => { ensureDir(root); writeJsonAtomic(path, s); };
|
|
70
|
+
const ack = op => ({ providerJobId: op.taskId, sessionId: op.logicalSessionId, turnId: op.taskId, environment: config.environment,
|
|
71
|
+
effective: { taskUrl: op.url, continuity: op.continuity, parentTaskId: op.parentTaskId, attemptSelector: 1, providerAttemptId: null, rootIdentity: "cloud-task", costSource: config.billing_source, tokenToolLogs: "unavailable" } });
|
|
72
|
+
const owned = (s, req) => {
|
|
73
|
+
const op = Object.values(s.operations).find(op => op.taskId === req.providerJobId);
|
|
74
|
+
if (!op || op.logicalSessionId !== req.sessionId || req.turnId !== op.taskId) throw executionError("EXECUTION_OWNERSHIP", "Cloud request does not own the exact saved task and single-attempt selector");
|
|
75
|
+
return op;
|
|
76
|
+
};
|
|
77
|
+
async function prepare(req) {
|
|
78
|
+
validateCloudConfig(config);
|
|
79
|
+
if (!repositoryMatches(req.repo, config.repository) || req.environment !== config.environment || req.mandatoryPolicy?.length) throw executionError("CLOUD_SOURCE", "selected repository/environment or policy differs");
|
|
80
|
+
const source = req.sources;
|
|
81
|
+
if (!source || !/^[a-f0-9]{40,64}$/.test(req.inputHead || "") || source.inputHead !== req.inputHead || source.repo !== req.repo || source.branch !== req.branch || source.inputBranch !== `${req.branch}-cloud-${req.inputHead}` || contentHash(Buffer.from(source.data || "", "base64")) !== source.hash) throw executionError("CLOUD_SOURCE", "Cloud needs an exact verified pushed snapshot and immutable input branch");
|
|
82
|
+
if (typeof req.prompt !== "string" || Buffer.byteLength(req.prompt) > 65_536 || /(?:\/Users\/|\/home\/|file:\/\/|[A-Z]:\\)/.test(req.prompt)) throw executionError("CLOUD_SOURCE", "Cloud input contains oversized context or absolute local paths; deliver approved repository snapshots");
|
|
83
|
+
await verification({ signal: req.signal });
|
|
84
|
+
const access = await checkAccess(config, { cli, signal: req.signal });
|
|
85
|
+
if (!access.ok) throw executionError(access.detail, access.fix);
|
|
86
|
+
if (!repositoryMatches(access.effective?.repository, req.repo) || access.effective.environment !== config.environment) throw executionError(CLOUD_DISPATCH_UNAVAILABLE.detail, CLOUD_DISPATCH_UNAVAILABLE.fix);
|
|
87
|
+
if (typeof verifyRemote !== "function") throw executionError("CLOUD_SOURCE", "Cloud dispatch requires the coordinator's remote revision/hash verifier");
|
|
88
|
+
await verifyRemote(req);
|
|
89
|
+
return { effective: access.effective };
|
|
90
|
+
}
|
|
91
|
+
async function dispatch(req, continuing = false) {
|
|
92
|
+
const s = read(), prior = s.operations[req.idempotencyKey];
|
|
93
|
+
if (prior) {
|
|
94
|
+
if (prior.taskId) return ack(prior);
|
|
95
|
+
throw executionError("EXECUTION_UNKNOWN", "Cloud acknowledgement unknown; inspect saved dispatch, never resubmit");
|
|
96
|
+
}
|
|
97
|
+
let previous;
|
|
98
|
+
try {
|
|
99
|
+
await prepare(req);
|
|
100
|
+
if (continuing) {
|
|
101
|
+
previous = Object.values(s.operations).findLast(op => op.logicalSessionId === req.sessionId && op.taskId);
|
|
102
|
+
if (!previous || !["succeeded", "failed"].includes(previous.state)) throw executionError("EXECUTION_CONTINUATION", "successor requires the same logical session and settled owned predecessor");
|
|
103
|
+
}
|
|
104
|
+
} catch (error) { throw dispatchNotSentError(error); }
|
|
105
|
+
const op = { runId: req.runId, name: req.name, key: req.idempotencyKey, inputHead: req.inputHead, repo: req.repo, branch: req.branch, base: req.base, sources: req.sources,
|
|
106
|
+
taskId: null, logicalSessionId: req.sessionId || null, parentTaskId: previous?.taskId || null, continuity: continuing ? "successor-task" : "initial-task", state: "status-unknown", at: clock.now(), acceptance: "unknown" };
|
|
107
|
+
const envelope = { version: 1, inputHead: op.inputHead, repo: op.repo, branch: op.branch, runId: op.runId, sourceHashes: op.sources.hashes, pr: { title: "<neutral change title>", body: "<complete approved completion receipt>" }, tests: [] };
|
|
108
|
+
const prompt = `${req.prompt}\n\nCloud delivery contract: the coordinator is the only PR writer. Do not push, open/update a PR or contact reviewers. Verify git rev-parse HEAD equals ${op.inputHead} BEFORE making changes; stop on mismatch. Verify approved source SHA-256 hashes: ${JSON.stringify(op.sources.hashes)}. Read the approved brief and sources on this revision; local paths are not delivery.\nContinuity: ${op.continuity}; predecessor: ${op.parentTaskId || "none"}. No native session memory is transferred; the supplied current code, requirements, feedback and decisions are authoritative.\nWrite ${CLOUD_RESULT_PATH} as JSON following this exact identity envelope: ${JSON.stringify(envelope)}. Replace title/body with the actual neutral change and truthful criterion receipt. Record each actual test in tests using command (string), inputHead (exactly ${op.inputHead}), exit (integer), and output (actual output); absent tests stay an empty list. Include this file in the returned diff with all implementation changes. An approved successor may return only this envelope when source changes are unnecessary, including receipt/public-text corrections or their explicit resume; do not invent unrelated edits. The coordinator excludes the envelope from the PR, executes approved Verify commands, and publishes exactly one PR.`;
|
|
109
|
+
if (Buffer.byteLength(prompt) > 100_000) throw dispatchNotSentError(executionError("CLOUD_SIZE", "serialized Cloud delivery contract too large; reduce approved context before dispatch"));
|
|
110
|
+
s.operations[req.idempotencyKey] = op; save(s);
|
|
111
|
+
const output = (await cli(["cloud", "exec", "--env", config.environment, "--branch", op.sources.inputBranch, "--attempts", "1", prompt], req.signal)).trim();
|
|
112
|
+
const urls = output.split(/\s+/).filter(value => /^https:\/\/chatgpt\.com\/codex\/tasks\/task_[A-Za-z0-9_-]+$/.test(value));
|
|
113
|
+
if (urls.length !== 1) throw executionError("EXECUTION_UNKNOWN", "Cloud submission finished without an exact task URL; preserve intent for manual reconciliation");
|
|
114
|
+
op.taskId = taskIdFor(urls[0].split("/").at(-1)); op.url = urls[0]; op.logicalSessionId ||= op.taskId; op.acceptance = "accepted"; op.state = "queued"; op.acknowledgedAt = clock.now(); save(s);
|
|
115
|
+
return ack(op);
|
|
116
|
+
}
|
|
117
|
+
async function observe(req) {
|
|
118
|
+
const s = read(), op = owned(s, req); let cursor = null, totalBytes = 0; const seen = new Set();
|
|
119
|
+
await checkCloudVersion(cli, req.signal);
|
|
120
|
+
for (let n = 0; n < 20; n++) {
|
|
121
|
+
const page = await listPage(cli, config, req.signal, cursor); totalBytes += Buffer.byteLength(JSON.stringify(page));
|
|
122
|
+
if (totalBytes > 1_048_576) throw executionError("CLOUD_SIZE", "Cloud recovery listing exceeds 1 MiB; narrow environment or recover manually");
|
|
123
|
+
const matches = page.tasks.filter(task => task.id === op.taskId);
|
|
124
|
+
if (matches.length > 1) throw executionError("CLOUD_PROTOCOL", "duplicate Cloud listing identity");
|
|
125
|
+
if (matches.length === 1) {
|
|
126
|
+
const task = matches[0], state = stateFor(task.status);
|
|
127
|
+
// 0.160.0 resolves --env before listing, but its list mapper leaves the
|
|
128
|
+
// row's environment_id null. Every page is scoped by listPage; reject
|
|
129
|
+
// contradictory metadata when supplied without requiring that field.
|
|
130
|
+
// Missing sibling metadata cannot contradict our owned --attempts 1
|
|
131
|
+
// dispatch; an explicit non-single count still refuses collection.
|
|
132
|
+
if (!state || (task.environment_id != null && task.environment_id !== config.environment) || task.url !== taskUrl(op.taskId) || (task.attempt_total != null && task.attempt_total !== 1)) throw executionError("CLOUD_PROTOCOL", "Cloud task environment/status/attempt ownership cannot be proven; best-of-N results are not selected implicitly");
|
|
133
|
+
const observedAt = clock.now(); op.state = state; op.observedAt = observedAt; op.summary = task.summary; op.updatedAt = task.updated_at; save(s);
|
|
134
|
+
const data = { state, providerStatus: task.status, environment: config.environment, environmentSource: "filtered-listing", summary: task.summary, providerUpdatedAt: task.updated_at, taskUrl: op.url, attemptSelector: 1, providerAttemptTotal: task.attempt_total ?? null, providerAttemptId: null, diffAvailable: state === "succeeded" && task.summary?.files_changed > 0, error: state === "failed" ? "provider reports error; detailed tool/token logs unavailable" : null, tokenToolLogs: "unavailable" };
|
|
135
|
+
return { ...ack(op), state, observedAt, costUsd: null, events: [{ eventId: `cloud:${op.taskId}:${contentHash(JSON.stringify(data))}`, timestamp: observedAt, type: "status", provenance: "provider-listing", data }] };
|
|
136
|
+
}
|
|
137
|
+
if (page.cursor == null) throw executionError("CLOUD_NOT_FOUND", "exact saved Cloud task absent; retain unknown status and task link, never adopt newest task");
|
|
138
|
+
if (seen.has(page.cursor)) throw executionError("CLOUD_PROTOCOL", "repeated Cloud pagination cursor");
|
|
139
|
+
seen.add(page.cursor); cursor = page.cursor;
|
|
140
|
+
}
|
|
141
|
+
throw executionError("CLOUD_SIZE", "Cloud listing exceeds bounded 20-page recovery; no implicit task adoption");
|
|
142
|
+
}
|
|
143
|
+
async function inspect(req) {
|
|
144
|
+
try { return await observe(req); }
|
|
145
|
+
catch (error) { error.providerObservationError = true; throw error; }
|
|
146
|
+
}
|
|
147
|
+
async function collect(req) {
|
|
148
|
+
const s = read(), op = owned(s, req);
|
|
149
|
+
if (req.inputHead !== op.inputHead || !["succeeded", "failed"].includes(op.state)) throw executionError("EXECUTION_RESULT", "Cloud task/base has not authoritatively completed");
|
|
150
|
+
await checkCloudVersion(cli, req.signal);
|
|
151
|
+
const outcome = { ok: op.state === "succeeded", finished: true, cost: null, lastMessage: null, denials: [], error: op.state === "failed" ? "Cloud task failed; inspect retained native task link" : null };
|
|
152
|
+
if (!outcome.ok) return { ...ack(op), inputHead: op.inputHead, outcome };
|
|
153
|
+
const diff = await cli(["cloud", "diff", op.taskId, "--attempt", "1"], req.signal);
|
|
154
|
+
const result = await cloudDiffResult(diff, op, { signal: req.signal });
|
|
155
|
+
return { ...result, ...ack(op), inputHead: op.inputHead, outcome };
|
|
156
|
+
}
|
|
157
|
+
return { prepare, submit: req => dispatch(req), continue: req => dispatch(req, true), inspect, collect,
|
|
158
|
+
lookup: async req => { const op = read().operations[req.intent.idempotencyKey]; return op?.taskId ? inspect({ ...req, ...ack(op) }) : null; },
|
|
159
|
+
cancel: async () => ({ confirmed: false, unsupported: true }) };
|
|
160
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { executionError } from "./execution_io.mjs";
|
|
3
|
+
|
|
4
|
+
export const CLOUD_VERSION = "0.160.0";
|
|
5
|
+
export const CLOUD_DISPATCH_UNAVAILABLE = Object.freeze({
|
|
6
|
+
detail: "CLOUD_ENVIRONMENT_REPOSITORY",
|
|
7
|
+
reason: `Codex Cloud ${CLOUD_VERSION} cannot verify the selected environment's connected repository; new dispatch is unavailable`,
|
|
8
|
+
fix: "Qualify and implement a supported documented environment-to-repository check before enabling Cloud dispatch; see docs/codex-cloud.md",
|
|
9
|
+
});
|
|
10
|
+
export function validateCloudConfig(config) {
|
|
11
|
+
const fail = message => { throw executionError("CLOUD_CONFIG", message); };
|
|
12
|
+
if (!config || typeof config.bin !== "string" || !isAbsolute(config.bin) || /[\x00-\x1f]/.test(config.bin)) fail("codex-cloud requires an explicit absolute Codex executable path");
|
|
13
|
+
if (config.account !== "operator" || config.authentication !== "operator-chatgpt" || config.mode !== "trusted-worker") fail("Cloud supports the authorized operator ChatGPT identity on a trusted worker; delegated public authentication is unqualified");
|
|
14
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(config.environment || "") || !/^[\w.-]+\/[\w.-]+$/.test(config.repository || "")) fail("select a published Cloud environment ID and its connected repository explicitly");
|
|
15
|
+
if (config.continuation !== "successor-task" || config.billing_source !== "chatgpt-account") fail("configure successor-task continuity and chatgpt-account billing; native resume/cancel and per-task spend are unavailable");
|
|
16
|
+
if (config.model != null || config.attempts != null && config.attempts !== 1) fail("Cloud preserves account model defaults and supports exactly one attempt; model overrides and best-of-N are unsupported");
|
|
17
|
+
if (!Number.isFinite(config.allowance?.remainingUsd) || !Number.isFinite(config.allowance?.reservedUsd) || config.allowance.reservedUsd <= 0 || config.allowance.remainingUsd < config.allowance.reservedUsd) throw executionError("EXECUTION_ALLOWANCE", "Cloud requires a positive reservation and remaining account allowance; this is not a provider billing cap");
|
|
18
|
+
if (config.mandatory_policy?.length || config.context_paths?.length) throw executionError("EXECUTION_POLICY", "Cloud cannot enforce mandatory local policy or private read-only context; use a capable backend");
|
|
19
|
+
return config;
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { prepareAgentsSources, contentHash, portablePath } from "./openai_agents_sources.mjs";
|
|
2
|
+
import { validateCloudConfig } from "./codex_cloud_config.mjs";
|
|
3
|
+
import { executionError } from "./execution_io.mjs";
|
|
4
|
+
import { isOurBranch } from "./git.mjs";
|
|
5
|
+
import { remoteMatches, repositoryMatches } from "./gh.mjs";
|
|
6
|
+
import { acquirePublicationLock } from "./execution_result.mjs";
|
|
7
|
+
import { executionLive } from "./execution.mjs";
|
|
8
|
+
import { CLOUD_RESULT_PATH } from "./codex_cloud_result.mjs";
|
|
9
|
+
import { taskProviderAllowance } from "./task_allowance.mjs";
|
|
10
|
+
|
|
11
|
+
// Called by the same initial/resume/review dispatch boundary as the API adapter.
|
|
12
|
+
// Commit only the already approved overlays in the task's isolated checkout.
|
|
13
|
+
export async function cloudInput(ctx, id, entry, input, contextPaths = []) {
|
|
14
|
+
const execution = ctx.executionFor?.(id, entry.harness);
|
|
15
|
+
const lock = ctx.home ? acquirePublicationLock(ctx, id, { execution, ownsExecution: true }) : null;
|
|
16
|
+
try {
|
|
17
|
+
const saved = execution?.state();
|
|
18
|
+
if (entry.dispatch_stopped || saved?.stopped || saved?.current && executionLive(saved.current)) throw executionError("BUSY", "Cloud input cannot mutate an active or stopped task checkout");
|
|
19
|
+
return await deliverCloudInput(ctx, id, entry, input, contextPaths);
|
|
20
|
+
} finally { lock?.release(); }
|
|
21
|
+
}
|
|
22
|
+
async function deliverCloudInput(ctx, id, entry, input, contextPaths) {
|
|
23
|
+
const config = validateCloudConfig(ctx.config.harness[entry.harness]);
|
|
24
|
+
if (contextPaths.length) throw executionError("EXECUTION_POLICY", "Cloud cannot deliver private absolute context paths");
|
|
25
|
+
if (!repositoryMatches(entry.repo, config.repository) || !isOurBranch(entry.branch) || entry.branch === entry.base || entry.worktree === entry.checkout ||
|
|
26
|
+
await ctx.git.currentBranch(entry.worktree) !== entry.branch || !remoteMatches(await ctx.git.originOf(entry.worktree), entry.repo)) throw executionError("CLOUD_SOURCE", "Cloud requires its configured repository and isolated owned task checkout");
|
|
27
|
+
const overlays = [entry.prd && { path: entry.prd, text: ctx.readPrd(id) }, ...(entry.sources || []).map(path => ({ path, text: ctx.readSource(id, path) }))].filter(Boolean);
|
|
28
|
+
const snapshot = await prepareAgentsSources({ worktree: entry.worktree, inputHead: input.inputHead, repo: entry.repo, branch: entry.branch, base: entry.base, overlays });
|
|
29
|
+
if (Object.hasOwn(snapshot.hashes, CLOUD_RESULT_PATH)) throw executionError("CLOUD_SOURCE", "reserved Cloud result envelope path exists in the approved input; choose another repository layout before dispatch");
|
|
30
|
+
if (!await ctx.git.resultModesMatch(entry.worktree, input.inputHead, overlays.map(item => item.path))) throw executionError("CLOUD_SOURCE", "approved snapshot mode changed; preserve the checkout and repair input before dispatch");
|
|
31
|
+
await ctx.git.commitResult(entry.worktree, overlays.map(item => portablePath(item.path)));
|
|
32
|
+
const inputHead = await ctx.git.head(entry.worktree);
|
|
33
|
+
await ctx.git.pushResult(entry.worktree, entry.branch);
|
|
34
|
+
if (await ctx.gh.refSha(entry.repo, entry.branch) !== inputHead) throw executionError("CLOUD_SOURCE", "remote task branch does not match the exact committed input head");
|
|
35
|
+
const hashes = {};
|
|
36
|
+
for (const item of overlays) {
|
|
37
|
+
const text = await ctx.gh.fileAtStrict(entry.repo, item.path, inputHead);
|
|
38
|
+
if (text !== item.text) throw executionError("CLOUD_SOURCE", "approved snapshot remote bytes differ; nothing dispatched");
|
|
39
|
+
hashes[item.path] = contentHash(text);
|
|
40
|
+
}
|
|
41
|
+
// A unique input ref is never advanced by the coordinator. Cloud reads this
|
|
42
|
+
// revision even if a reviewer/operator later advances the logical PR branch.
|
|
43
|
+
const inputBranch = `${entry.branch}-cloud-${inputHead}`;
|
|
44
|
+
await ctx.git.pushSnapshotRef(entry.worktree, inputBranch);
|
|
45
|
+
if (await ctx.gh.refSha(entry.repo, inputBranch) !== inputHead || await ctx.git.head(entry.worktree) !== inputHead) throw executionError("CLOUD_SOURCE", "Cloud input revision moved before dispatch");
|
|
46
|
+
const sources = await prepareAgentsSources({ worktree: entry.worktree, inputHead, repo: entry.repo, branch: entry.branch, base: entry.base, overlays: [] });
|
|
47
|
+
return { ...input, inputHead, repo: entry.repo, branch: entry.branch, base: entry.base, environment: config.environment, allowance: taskProviderAllowance(config.allowance, entry.task),
|
|
48
|
+
mandatoryPolicy: config.mandatory_policy || [], sources: { data: sources.bytes.toString("base64"), hash: sources.hash, hashes, inputHead, inputBranch, repo: entry.repo, branch: entry.branch, snapshotHash: snapshot.hash } };
|
|
49
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Cloud diff -> bounded text manifest. Never apply a provider patch in the
|
|
2
|
+
// operator/task checkout; the shared result writer remains its only mutator.
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, chmodSync, readFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { gunzipSync } from "node:zlib";
|
|
7
|
+
import { execFileAsync } from "./gh.mjs";
|
|
8
|
+
import { runEnv } from "./git.mjs";
|
|
9
|
+
import { contentHash, portablePath } from "./openai_agents_sources.mjs";
|
|
10
|
+
import { assertRepositoryTargets, readRepositoryText } from "./repository_files.mjs";
|
|
11
|
+
import { collectionValidationError, validProviderTestRecords } from "./execution_io.mjs";
|
|
12
|
+
export const CLOUD_RESULT_PATH = ".codex-cloud-result.json";
|
|
13
|
+
export async function cloudDiffResult(diff, op, { signal, exec = execFileAsync } = {}) {
|
|
14
|
+
const fail = message => { throw collectionValidationError(message); };
|
|
15
|
+
let root;
|
|
16
|
+
try {
|
|
17
|
+
if (typeof diff !== "string" || !diff.startsWith("diff --git ") || Buffer.byteLength(diff) > 1_048_576 || diff.includes("\0")) fail("missing, binary or oversized Cloud diff");
|
|
18
|
+
const paths = [], headers = [];
|
|
19
|
+
for (const block of diff.split(/^diff --git /m).slice(1)) {
|
|
20
|
+
const header = block.slice(0, block.indexOf("\n"));
|
|
21
|
+
const match = header.match(/^a\/(.+) b\/\1$/);
|
|
22
|
+
if (!match) fail("Cloud patch has a renamed, quoted or unsupported path; retain the diff for explicit recovery");
|
|
23
|
+
const path = portablePath(match[1]); paths.push(path);
|
|
24
|
+
const metadata = block.split(/^@@/m)[0];
|
|
25
|
+
if (/^(?:old mode|new mode|rename |copy |GIT binary|Binary files)/m.test(metadata) || /^new file mode (?!100644$)/m.test(metadata) || /^index [^\n]+ (?!100644|100755)\d{6}$/m.test(metadata) || /^deleted file mode (?!100644|100755)\d{6}$/m.test(metadata)) fail("unsupported Cloud file type/mode change");
|
|
26
|
+
const old = metadata.match(/^--- (.+)$/m)?.[1], next = metadata.match(/^\+\+\+ (.+)$/m)?.[1];
|
|
27
|
+
if (!old || !next || !["/dev/null", `a/${path}`].includes(old) || !["/dev/null", `b/${path}`].includes(next) || old === next) fail("Cloud diff header paths disagree");
|
|
28
|
+
headers.push({ path, creation: old === "/dev/null", mode: metadata.match(/^index [^\n]+ (\d{6})$/m)?.[1] || metadata.match(/^deleted file mode (\d{6})$/m)?.[1] });
|
|
29
|
+
}
|
|
30
|
+
if (!paths.length || paths.length > 101 || new Set(paths).size !== paths.length || !paths.includes(CLOUD_RESULT_PATH)) fail("duplicate, excessive or missing Cloud diff/envelope paths");
|
|
31
|
+
const raw = Buffer.from(op.sources.data, "base64");
|
|
32
|
+
if (contentHash(raw) !== op.sources.hash) fail("saved Cloud snapshot hash differs");
|
|
33
|
+
const source = JSON.parse(gunzipSync(raw, { maxOutputLength: 12 * 1024 * 1024 }).toString("utf8"));
|
|
34
|
+
if (source.inputHead !== op.inputHead || source.repo !== op.repo || source.branch !== op.branch) fail("Cloud snapshot base/ownership differs");
|
|
35
|
+
const before = new Map(source.files.map(file => [portablePath(file.path), file]));
|
|
36
|
+
for (const header of headers) {
|
|
37
|
+
const file = before.get(header.path);
|
|
38
|
+
if (header.creation === Boolean(file) || header.mode && header.mode !== `100${file?.mode.toString(8)}`) fail("Cloud diff base file presence/mode disagrees with the exact input snapshot");
|
|
39
|
+
}
|
|
40
|
+
if (before.has(CLOUD_RESULT_PATH)) fail("reserved Cloud result path already exists in source");
|
|
41
|
+
assertRepositoryTargets(paths, [...before.keys()]);
|
|
42
|
+
root = mkdtempSync(join(tmpdir(), "cloud-diff-"));
|
|
43
|
+
const checkout = join(root, "checkout"); mkdirSync(checkout);
|
|
44
|
+
for (const file of source.files) {
|
|
45
|
+
const path = portablePath(file.path), bytes = Buffer.from(file.data, "base64");
|
|
46
|
+
if (contentHash(bytes) !== file.hash || ![0o644, 0o755].includes(file.mode)) fail("saved Cloud source bytes/mode differ");
|
|
47
|
+
const target = join(checkout, path); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, bytes); chmodSync(target, file.mode);
|
|
48
|
+
}
|
|
49
|
+
const patch = join(root, "result.diff"); writeFileSync(patch, diff, { mode: 0o600 });
|
|
50
|
+
const options = { cwd: checkout, signal, env: { PATH: process.env.PATH, ...runEnv(), HOME: root }, timeout: 5000, maxBuffer: 65_536 };
|
|
51
|
+
try { await exec("git", ["apply", "--check", "--", patch], options); await exec("git", ["apply", "--", patch], options); }
|
|
52
|
+
catch (error) { if (signal?.aborted || typeof error.code !== "number" || error.killed) throw error; fail("Cloud diff does not apply to the exact saved input revision"); }
|
|
53
|
+
const receipt = JSON.parse(readRepositoryText(checkout, CLOUD_RESULT_PATH));
|
|
54
|
+
const sorted = hashes => hashes && typeof hashes === "object" && !Array.isArray(hashes) ? JSON.stringify(Object.entries(hashes).sort(([a], [b]) => a.localeCompare(b))) : null;
|
|
55
|
+
if (receipt.version !== 1 || receipt.inputHead !== op.inputHead || receipt.repo !== op.repo || receipt.branch !== op.branch || receipt.runId !== op.runId || sorted(receipt.sourceHashes) !== sorted(op.sources.hashes) || !receipt.pr || !validProviderTestRecords(receipt.tests, op.inputHead) || receipt.tests.length > 100) fail("Cloud result envelope does not own this run/base/source snapshot or has incomplete test records");
|
|
56
|
+
const files = paths.filter(path => path !== CLOUD_RESULT_PATH).map(path => {
|
|
57
|
+
let text = null;
|
|
58
|
+
try { text = readRepositoryText(checkout, path); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
59
|
+
const bytes = text === null ? null : readFileSync(join(checkout, path));
|
|
60
|
+
if (text !== null && (text.includes("\0") || !Buffer.from(text).equals(bytes))) fail("Cloud text result is binary or invalid UTF-8");
|
|
61
|
+
return { path, beforeHash: before.get(path)?.hash ?? null, afterHash: contentHash(text), text };
|
|
62
|
+
});
|
|
63
|
+
const textOnly = !files.length && op.continuity === "successor-task";
|
|
64
|
+
if (!files.length && !textOnly) fail("Cloud diff has no deliverable file changes outside an approved successor");
|
|
65
|
+
return { repo: op.repo, branch: op.branch, files, pr: receipt.pr, tests: receipt.tests,
|
|
66
|
+
// Reuse the shared writer's exact pushed-revision path. It rechecks remote
|
|
67
|
+
// head/ancestry, clean checkout and Verify evidence before updating one PR.
|
|
68
|
+
...(textOnly ? { pushedHead: op.inputHead } : {}),
|
|
69
|
+
artifact: { source: "codex cloud diff --attempt 1", hash: contentHash(diff), inputHead: op.inputHead } };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (signal?.aborted || error.collectionValidation || error.killed || /^(?:ENOENT|EACCES|EPERM|EIO|ENOSPC|EROFS|EMFILE|ENFILE|ENOMEM|ETIMEDOUT|ABORT_ERR)$/.test(error.code || "")) throw error;
|
|
72
|
+
throw collectionValidationError("invalid Cloud result paths, snapshot or delivery envelope");
|
|
73
|
+
} finally { if (root) rmSync(root, { recursive: true, force: true }); }
|
|
74
|
+
}
|