@bitkyc08/opencodex 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { FABRIC_LIMITS, SYNTHETIC_VALUE_PATH } from "./constants";
|
|
2
|
+
import { assertSafeRelativePosixPath, writeScratchFileUtf8 } from "./scratch";
|
|
3
|
+
import { FabricTaskError, type SyntheticPatchV1 } from "./types";
|
|
4
|
+
|
|
5
|
+
const ALLOWED_KEYS = new Set(["schemaVersion", "operations"]);
|
|
6
|
+
const OP_KEYS = new Set(["op", "path", "contentUtf8"]);
|
|
7
|
+
|
|
8
|
+
/** Parse and validate a synthetic-patch producer payload. */
|
|
9
|
+
export function parseSyntheticPatchV1(raw: unknown): SyntheticPatchV1 {
|
|
10
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
11
|
+
throw new FabricTaskError("malformed producer outcome: patch must be object", "malformed_producer_outcome", "harness");
|
|
12
|
+
}
|
|
13
|
+
const obj = raw as Record<string, unknown>;
|
|
14
|
+
for (const key of Object.keys(obj)) {
|
|
15
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
16
|
+
throw new FabricTaskError(`malformed producer outcome: unknown field ${key}`, "malformed_producer_outcome", "harness");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (obj.schemaVersion !== 1) {
|
|
20
|
+
throw new FabricTaskError("malformed producer outcome: schemaVersion", "malformed_producer_outcome", "harness");
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(obj.operations)) {
|
|
23
|
+
throw new FabricTaskError("malformed producer outcome: operations", "malformed_producer_outcome", "harness");
|
|
24
|
+
}
|
|
25
|
+
if (obj.operations.length !== 1) {
|
|
26
|
+
throw new FabricTaskError("exactly one patch operation is required", "malformed_producer_outcome", "harness");
|
|
27
|
+
}
|
|
28
|
+
const opRaw = obj.operations[0];
|
|
29
|
+
if (!opRaw || typeof opRaw !== "object" || Array.isArray(opRaw)) {
|
|
30
|
+
throw new FabricTaskError("malformed producer outcome: operation", "malformed_producer_outcome", "harness");
|
|
31
|
+
}
|
|
32
|
+
const op = opRaw as Record<string, unknown>;
|
|
33
|
+
for (const key of Object.keys(op)) {
|
|
34
|
+
if (!OP_KEYS.has(key)) {
|
|
35
|
+
throw new FabricTaskError(`malformed producer outcome: unknown op field ${key}`, "malformed_producer_outcome", "harness");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (op.op !== "replace" || typeof op.path !== "string" || typeof op.contentUtf8 !== "string") {
|
|
39
|
+
throw new FabricTaskError("malformed producer outcome: replace shape", "malformed_producer_outcome", "harness");
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
assertSafeRelativePosixPath(op.path);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof FabricTaskError) throw error;
|
|
45
|
+
throw new FabricTaskError("patch path escapes scratch", "sandbox_violation", "harness");
|
|
46
|
+
}
|
|
47
|
+
if (op.path !== SYNTHETIC_VALUE_PATH) {
|
|
48
|
+
throw new FabricTaskError("patch path must be src/value.txt", "behavioral_failure", "route");
|
|
49
|
+
}
|
|
50
|
+
const contentBytes = Buffer.byteLength(op.contentUtf8, "utf8");
|
|
51
|
+
if (contentBytes > FABRIC_LIMITS.maxAggregateIoBytes) {
|
|
52
|
+
throw new FabricTaskError("patch content exceeds io budget", "budget_exhausted", "environment");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
operations: [{ op: "replace", path: SYNTHETIC_VALUE_PATH, contentUtf8: op.contentUtf8 }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Apply a validated synthetic patch inside the scratch tree under budget limits. */
|
|
61
|
+
export function applySyntheticPatch(scratchRoot: string, patch: SyntheticPatchV1): { bytesWritten: number; filesTouched: number; patchOperations: number } {
|
|
62
|
+
if (patch.operations.length > FABRIC_LIMITS.maxPatchOperations) {
|
|
63
|
+
throw new FabricTaskError("too many patch operations", "budget_exhausted", "environment");
|
|
64
|
+
}
|
|
65
|
+
let bytesWritten = 0;
|
|
66
|
+
for (const operation of patch.operations) {
|
|
67
|
+
bytesWritten += writeScratchFileUtf8(
|
|
68
|
+
scratchRoot,
|
|
69
|
+
operation.path,
|
|
70
|
+
operation.contentUtf8,
|
|
71
|
+
FABRIC_LIMITS.maxAggregateIoBytes,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
bytesWritten,
|
|
76
|
+
filesTouched: patch.operations.length,
|
|
77
|
+
patchOperations: patch.operations.length,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FABRIC_LIMITS,
|
|
3
|
+
SYNTHETIC_AFTER_UTF8,
|
|
4
|
+
SYNTHETIC_BEFORE_UTF8,
|
|
5
|
+
SYNTHETIC_VALUE_PATH,
|
|
6
|
+
} from "./constants";
|
|
7
|
+
import {
|
|
8
|
+
encodeProducerProtocolLine,
|
|
9
|
+
FABRIC_PRODUCER_PROTOCOL_MAX_BYTES,
|
|
10
|
+
FABRIC_PRODUCER_REQUEST_MAX_BYTES,
|
|
11
|
+
type ProducerParentRequest,
|
|
12
|
+
} from "./producer-protocol";
|
|
13
|
+
import type { FabricHarnessProducerKind, SyntheticPatchV1 } from "./types";
|
|
14
|
+
|
|
15
|
+
function writeLine(message: Parameters<typeof encodeProducerProtocolLine>[0]): void {
|
|
16
|
+
process.stdout.write(encodeProducerProtocolLine(message));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function correctPatch(): SyntheticPatchV1 {
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
operations: [{ op: "replace", path: SYNTHETIC_VALUE_PATH, contentUtf8: SYNTHETIC_AFTER_UTF8 }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wrongPatch(): SyntheticPatchV1 {
|
|
27
|
+
return {
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
operations: [{ op: "replace", path: SYNTHETIC_VALUE_PATH, contentUtf8: "wrong\n" }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runHarness(
|
|
34
|
+
kind: FabricHarnessProducerKind,
|
|
35
|
+
scratchRoot: string,
|
|
36
|
+
totalTimeoutMs: number,
|
|
37
|
+
inactivityTimeoutMs: number,
|
|
38
|
+
): Promise<SyntheticPatchV1> {
|
|
39
|
+
switch (kind) {
|
|
40
|
+
case "deterministic_correct":
|
|
41
|
+
return correctPatch();
|
|
42
|
+
case "deterministic_wrong":
|
|
43
|
+
return wrongPatch();
|
|
44
|
+
case "infinite_sync":
|
|
45
|
+
while (true) {
|
|
46
|
+
/* terminated by parent */
|
|
47
|
+
}
|
|
48
|
+
case "never_resolve":
|
|
49
|
+
while (true) {
|
|
50
|
+
await Bun.sleep(Math.max(25, Math.min(1_000, Math.floor(inactivityTimeoutMs * 0.75))));
|
|
51
|
+
}
|
|
52
|
+
case "mutate_after_delay": {
|
|
53
|
+
await Bun.sleep(totalTimeoutMs + Math.max(250, inactivityTimeoutMs));
|
|
54
|
+
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
55
|
+
const { join, dirname } = await import("node:path");
|
|
56
|
+
mkdirSync(join(scratchRoot, dirname(SYNTHETIC_VALUE_PATH)), { recursive: true });
|
|
57
|
+
writeFileSync(join(scratchRoot, SYNTHETIC_VALUE_PATH), "late\n");
|
|
58
|
+
return correctPatch();
|
|
59
|
+
}
|
|
60
|
+
case "periodic_activity": {
|
|
61
|
+
const intervalMs = Math.max(10, Math.min(200, Math.floor(inactivityTimeoutMs * 0.4)));
|
|
62
|
+
const iterations = Math.max(3, Math.min(20, Math.floor((totalTimeoutMs * 0.7) / intervalMs)));
|
|
63
|
+
for (let i = 0; i < iterations; i++) {
|
|
64
|
+
writeLine({ type: "activity" });
|
|
65
|
+
await Bun.sleep(intervalMs);
|
|
66
|
+
}
|
|
67
|
+
return correctPatch();
|
|
68
|
+
}
|
|
69
|
+
case "activity_until_total": {
|
|
70
|
+
const intervalMs = Math.max(10, Math.min(50, Math.floor(inactivityTimeoutMs * 0.25)));
|
|
71
|
+
while (true) {
|
|
72
|
+
writeLine({ type: "activity" });
|
|
73
|
+
await Bun.sleep(intervalMs);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
case "flood_stdout":
|
|
77
|
+
while (true) {
|
|
78
|
+
process.stdout.write("x".repeat(4096));
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(`unsupported harness kind: ${kind as string}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runExecutorModule(
|
|
86
|
+
modulePath: string,
|
|
87
|
+
executorInput: unknown,
|
|
88
|
+
reportActivity: () => void,
|
|
89
|
+
): Promise<SyntheticPatchV1> {
|
|
90
|
+
const mod = await import(modulePath) as { execute: (input: unknown) => Promise<SyntheticPatchV1> | SyntheticPatchV1 };
|
|
91
|
+
if (typeof mod.execute !== "function") throw new Error("executor module missing execute export");
|
|
92
|
+
const input = {
|
|
93
|
+
...(executorInput as Record<string, unknown>),
|
|
94
|
+
reportActivity,
|
|
95
|
+
};
|
|
96
|
+
return await Promise.resolve(mod.execute(input));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function main(): Promise<void> {
|
|
100
|
+
const raw = await Bun.stdin.text();
|
|
101
|
+
if (Buffer.byteLength(raw, "utf8") > FABRIC_PRODUCER_REQUEST_MAX_BYTES) {
|
|
102
|
+
writeLine({
|
|
103
|
+
type: "error",
|
|
104
|
+
code: "budget_exhausted",
|
|
105
|
+
message: "request payload exceeds protocol limit",
|
|
106
|
+
attribution: "environment",
|
|
107
|
+
});
|
|
108
|
+
process.exit(1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const req = JSON.parse(raw) as ProducerParentRequest;
|
|
112
|
+
const totalTimeoutMs = req.totalTimeoutMs ?? FABRIC_LIMITS.totalTimeoutMs;
|
|
113
|
+
const inactivityTimeoutMs = req.inactivityTimeoutMs ?? FABRIC_LIMITS.inactivityTimeoutMs;
|
|
114
|
+
let patch: SyntheticPatchV1;
|
|
115
|
+
const reportActivity = () => writeLine({ type: "activity" });
|
|
116
|
+
if (req.harnessKind) {
|
|
117
|
+
patch = await runHarness(
|
|
118
|
+
req.harnessKind as FabricHarnessProducerKind,
|
|
119
|
+
req.scratchRoot,
|
|
120
|
+
totalTimeoutMs,
|
|
121
|
+
inactivityTimeoutMs,
|
|
122
|
+
);
|
|
123
|
+
} else if (req.executorModulePath) {
|
|
124
|
+
patch = await runExecutorModule(req.executorModulePath, req.executorInput, reportActivity);
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error("child request missing harnessKind or executorModulePath");
|
|
127
|
+
}
|
|
128
|
+
writeLine({ type: "result", patch });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch((error: unknown) => {
|
|
132
|
+
writeLine({
|
|
133
|
+
type: "error",
|
|
134
|
+
code: "harness_failure",
|
|
135
|
+
message: error instanceof Error ? error.message : String(error),
|
|
136
|
+
attribution: "harness",
|
|
137
|
+
});
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { FABRIC_LIMITS } from "./constants";
|
|
5
|
+
import {
|
|
6
|
+
FABRIC_PRODUCER_PROTOCOL_MAX_BYTES,
|
|
7
|
+
FABRIC_PRODUCER_REQUEST_MAX_BYTES,
|
|
8
|
+
FABRIC_PRODUCER_STDERR_MAX_BYTES,
|
|
9
|
+
parseProducerProtocolLine,
|
|
10
|
+
type IsolatedProducerResult,
|
|
11
|
+
} from "./producer-protocol";
|
|
12
|
+
import type { FabricHarnessProducerKind, FabricPatchExecutorInput, SyntheticPatchV1 } from "./types";
|
|
13
|
+
import { FabricTaskError } from "./types";
|
|
14
|
+
|
|
15
|
+
const CHILD_ENTRY = join(dirname(fileURLToPath(import.meta.url)), "producer-child.ts");
|
|
16
|
+
|
|
17
|
+
type FabricProducerIsolationLimits = {
|
|
18
|
+
totalTimeoutMs: number;
|
|
19
|
+
inactivityTimeoutMs: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let testIsolationLimits: FabricProducerIsolationLimits | undefined;
|
|
23
|
+
|
|
24
|
+
interface IsolateRequest {
|
|
25
|
+
harnessKind?: FabricHarnessProducerKind;
|
|
26
|
+
executorModulePath?: string;
|
|
27
|
+
scratchRoot: string;
|
|
28
|
+
totalTimeoutMs: number;
|
|
29
|
+
inactivityTimeoutMs: number;
|
|
30
|
+
executorInput?: FabricPatchExecutorInput;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function minimalChildEnv(scratchRoot: string): Record<string, string> {
|
|
35
|
+
return {
|
|
36
|
+
TZ: "UTC",
|
|
37
|
+
NO_COLOR: "1",
|
|
38
|
+
OCX_FABRIC_SCRATCH_ROOT: scratchRoot,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function killChild(child: ChildProcess): void {
|
|
43
|
+
try {
|
|
44
|
+
child.kill("SIGKILL");
|
|
45
|
+
} catch {
|
|
46
|
+
/* ignore */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Run a fabric patch producer in an isolated child process with parent-owned timeouts. */
|
|
51
|
+
export async function runIsolatedFabricProducer(request: IsolateRequest): Promise<IsolatedProducerResult> {
|
|
52
|
+
const now = request.now ?? (() => Date.now());
|
|
53
|
+
let lastActivityAt = now();
|
|
54
|
+
|
|
55
|
+
return await new Promise<IsolatedProducerResult>((resolve, reject) => {
|
|
56
|
+
let child: ChildProcess;
|
|
57
|
+
try {
|
|
58
|
+
child = spawn(process.execPath, ["run", CHILD_ENTRY], {
|
|
59
|
+
env: minimalChildEnv(request.scratchRoot),
|
|
60
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
reject(new FabricTaskError(
|
|
64
|
+
error instanceof Error ? error.message : String(error),
|
|
65
|
+
"harness_failure",
|
|
66
|
+
"harness",
|
|
67
|
+
));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let stdoutBuffer = "";
|
|
72
|
+
let stderrBytes = 0;
|
|
73
|
+
let settled = false;
|
|
74
|
+
let receivedResult: SyntheticPatchV1 | undefined;
|
|
75
|
+
let killReason: FabricTaskError | undefined;
|
|
76
|
+
|
|
77
|
+
const finish = (fn: () => void) => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
clearTimeout(totalTimer);
|
|
81
|
+
clearTimeout(inactivityTimer);
|
|
82
|
+
fn();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const settleTimeout = (error: FabricTaskError) => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
killReason = error;
|
|
88
|
+
killChild(child);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const armInactivity = () => {
|
|
92
|
+
clearTimeout(inactivityTimer);
|
|
93
|
+
inactivityTimer = setTimeout(() => {
|
|
94
|
+
settleTimeout(new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
|
|
95
|
+
}, request.inactivityTimeoutMs);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let inactivityTimer: ReturnType<typeof setTimeout> = setTimeout(() => {
|
|
99
|
+
settleTimeout(new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
|
|
100
|
+
}, request.inactivityTimeoutMs);
|
|
101
|
+
|
|
102
|
+
const totalTimer = setTimeout(() => {
|
|
103
|
+
settleTimeout(new FabricTaskError("total timeout exceeded", "timeout", "environment"));
|
|
104
|
+
}, request.totalTimeoutMs);
|
|
105
|
+
|
|
106
|
+
const handleProtocolLine = (line: string) => {
|
|
107
|
+
try {
|
|
108
|
+
const message = parseProducerProtocolLine(line);
|
|
109
|
+
if (message.type === "activity") {
|
|
110
|
+
lastActivityAt = now();
|
|
111
|
+
armInactivity();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (message.type === "result") {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
receivedResult = message.patch;
|
|
117
|
+
finish(() => resolve({ patch: message.patch, lastActivityAt }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (message.type === "error") {
|
|
121
|
+
const fabricCode = message.code === "inactivity_timeout"
|
|
122
|
+
? "inactivity_timeout"
|
|
123
|
+
: message.code === "timeout"
|
|
124
|
+
? "timeout"
|
|
125
|
+
: message.code === "sandbox_violation"
|
|
126
|
+
? "sandbox_violation"
|
|
127
|
+
: message.code === "budget_exhausted"
|
|
128
|
+
? "budget_exhausted"
|
|
129
|
+
: "harness_failure";
|
|
130
|
+
const attribution = message.attribution === "environment" ? "environment" : "harness";
|
|
131
|
+
const fabricError = new FabricTaskError(message.message, fabricCode, attribution);
|
|
132
|
+
finish(() => reject(fabricError));
|
|
133
|
+
killChild(child);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
settleTimeout(new FabricTaskError(
|
|
138
|
+
error instanceof Error ? error.message : String(error),
|
|
139
|
+
"harness_failure",
|
|
140
|
+
"harness",
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const consumeStdout = (chunk: string) => {
|
|
146
|
+
stdoutBuffer += chunk;
|
|
147
|
+
if (Buffer.byteLength(stdoutBuffer, "utf8") > FABRIC_PRODUCER_PROTOCOL_MAX_BYTES) {
|
|
148
|
+
settleTimeout(new FabricTaskError("producer protocol output exceeded limit", "budget_exhausted", "environment"));
|
|
149
|
+
stdoutBuffer = "";
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let newlineIdx = stdoutBuffer.indexOf("\n");
|
|
153
|
+
while (newlineIdx >= 0) {
|
|
154
|
+
const line = stdoutBuffer.slice(0, newlineIdx);
|
|
155
|
+
stdoutBuffer = stdoutBuffer.slice(newlineIdx + 1);
|
|
156
|
+
handleProtocolLine(line);
|
|
157
|
+
newlineIdx = stdoutBuffer.indexOf("\n");
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
162
|
+
consumeStdout(chunk.toString("utf8"));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
child.stdout?.on("error", (error) => {
|
|
166
|
+
if (settled) return;
|
|
167
|
+
finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
171
|
+
stderrBytes += Buffer.byteLength(chunk.toString("utf8"), "utf8");
|
|
172
|
+
if (stderrBytes > FABRIC_PRODUCER_STDERR_MAX_BYTES) {
|
|
173
|
+
stderrBytes = FABRIC_PRODUCER_STDERR_MAX_BYTES;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
child.on("error", (error) => {
|
|
178
|
+
finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
child.stdin?.on("error", (error: NodeJS.ErrnoException) => {
|
|
182
|
+
if (settled || error.code === "EPIPE") return;
|
|
183
|
+
killChild(child);
|
|
184
|
+
finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const payload = JSON.stringify({
|
|
188
|
+
harnessKind: request.harnessKind,
|
|
189
|
+
executorModulePath: request.executorModulePath,
|
|
190
|
+
scratchRoot: request.scratchRoot,
|
|
191
|
+
totalTimeoutMs: request.totalTimeoutMs,
|
|
192
|
+
inactivityTimeoutMs: request.inactivityTimeoutMs,
|
|
193
|
+
executorInput: request.executorInput
|
|
194
|
+
? {
|
|
195
|
+
routeContext: request.executorInput.routeContext,
|
|
196
|
+
destination: request.executorInput.destination,
|
|
197
|
+
routeSubject: request.executorInput.routeSubject,
|
|
198
|
+
scratchRoot: request.executorInput.scratchRoot,
|
|
199
|
+
}
|
|
200
|
+
: undefined,
|
|
201
|
+
});
|
|
202
|
+
if (Buffer.byteLength(payload, "utf8") > FABRIC_PRODUCER_REQUEST_MAX_BYTES) {
|
|
203
|
+
killChild(child);
|
|
204
|
+
finish(() => reject(new FabricTaskError("producer request exceeds protocol limit", "budget_exhausted", "environment")));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
child.stdin?.write(payload);
|
|
210
|
+
child.stdin?.end();
|
|
211
|
+
} catch (error) {
|
|
212
|
+
killChild(child);
|
|
213
|
+
finish(() => reject(new FabricTaskError(
|
|
214
|
+
error instanceof Error ? error.message : String(error),
|
|
215
|
+
"harness_failure",
|
|
216
|
+
"harness",
|
|
217
|
+
)));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
child.on("close", (code, signal) => {
|
|
222
|
+
if (settled) return;
|
|
223
|
+
if (killReason) {
|
|
224
|
+
finish(() => reject(killReason!));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (receivedResult) {
|
|
228
|
+
finish(() => resolve({ patch: receivedResult!, lastActivityAt }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (stdoutBuffer.trim()) {
|
|
232
|
+
try {
|
|
233
|
+
handleProtocolLine(stdoutBuffer.trim());
|
|
234
|
+
if (receivedResult) return;
|
|
235
|
+
} catch {
|
|
236
|
+
/* fall through */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (signal === "SIGKILL") {
|
|
240
|
+
finish(() => reject(new FabricTaskError("total timeout exceeded", "timeout", "environment")));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
finish(() => reject(new FabricTaskError(
|
|
244
|
+
code === 0 ? "isolated producer returned no result" : `isolated producer exited (${code ?? signal ?? "unknown"})`,
|
|
245
|
+
"harness_failure",
|
|
246
|
+
"harness",
|
|
247
|
+
)));
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Internal test seam. Production callers cannot arm it without the test-home guard. */
|
|
253
|
+
export function setFabricProducerIsolationLimitsForTests(limits?: FabricProducerIsolationLimits): void {
|
|
254
|
+
if (process.env.OCX_TEST_HOME_GUARD !== "1") {
|
|
255
|
+
throw new Error("fabric isolation limits can only be overridden by the test harness");
|
|
256
|
+
}
|
|
257
|
+
if (limits && (
|
|
258
|
+
!Number.isFinite(limits.totalTimeoutMs)
|
|
259
|
+
|| !Number.isFinite(limits.inactivityTimeoutMs)
|
|
260
|
+
|| limits.totalTimeoutMs <= 0
|
|
261
|
+
|| limits.inactivityTimeoutMs <= 0
|
|
262
|
+
|| limits.inactivityTimeoutMs >= limits.totalTimeoutMs
|
|
263
|
+
)) {
|
|
264
|
+
throw new Error("invalid fabric test isolation limits");
|
|
265
|
+
}
|
|
266
|
+
testIsolationLimits = limits ? { ...limits } : undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Default isolation limits for fabric producer child processes. */
|
|
270
|
+
export function fabricProducerIsolationLimits(): FabricProducerIsolationLimits {
|
|
271
|
+
if (testIsolationLimits) return { ...testIsolationLimits };
|
|
272
|
+
return {
|
|
273
|
+
totalTimeoutMs: FABRIC_LIMITS.totalTimeoutMs,
|
|
274
|
+
inactivityTimeoutMs: FABRIC_LIMITS.inactivityTimeoutMs,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SyntheticPatchV1 } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Maximum bytes accepted from child stdout for the fabric producer protocol. */
|
|
4
|
+
export const FABRIC_PRODUCER_PROTOCOL_MAX_BYTES = 64 * 1024;
|
|
5
|
+
|
|
6
|
+
/** Maximum stderr bytes retained for harness diagnostics. */
|
|
7
|
+
export const FABRIC_PRODUCER_STDERR_MAX_BYTES = 4 * 1024;
|
|
8
|
+
|
|
9
|
+
/** Maximum request payload size on child stdin. */
|
|
10
|
+
export const FABRIC_PRODUCER_REQUEST_MAX_BYTES = 64 * 1024;
|
|
11
|
+
|
|
12
|
+
export type ProducerChildMessage =
|
|
13
|
+
| { type: "activity" }
|
|
14
|
+
| { type: "result"; patch: SyntheticPatchV1 }
|
|
15
|
+
| { type: "error"; code: string; message: string; attribution: "harness" | "environment" };
|
|
16
|
+
|
|
17
|
+
export interface ProducerParentRequest {
|
|
18
|
+
harnessKind?: string;
|
|
19
|
+
executorModulePath?: string;
|
|
20
|
+
scratchRoot: string;
|
|
21
|
+
executorInput?: unknown;
|
|
22
|
+
/** Parent-owned budgets are passed through so synthetic harness timing can scale in tests. */
|
|
23
|
+
totalTimeoutMs?: number;
|
|
24
|
+
inactivityTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IsolatedProducerResult {
|
|
28
|
+
patch: SyntheticPatchV1;
|
|
29
|
+
lastActivityAt: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Serialize one protocol line to stdout (newline-delimited JSON). */
|
|
33
|
+
export function encodeProducerProtocolLine(message: ProducerChildMessage): string {
|
|
34
|
+
return `${JSON.stringify(message)}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse one protocol line from the child. Malformed lines throw. */
|
|
38
|
+
export function parseProducerProtocolLine(line: string): ProducerChildMessage {
|
|
39
|
+
const trimmed = line.trim();
|
|
40
|
+
if (!trimmed) {
|
|
41
|
+
throw new Error("empty protocol line");
|
|
42
|
+
}
|
|
43
|
+
const parsed = JSON.parse(trimmed) as ProducerChildMessage;
|
|
44
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string") {
|
|
45
|
+
throw new Error("malformed protocol message");
|
|
46
|
+
}
|
|
47
|
+
switch (parsed.type) {
|
|
48
|
+
case "activity":
|
|
49
|
+
return { type: "activity" };
|
|
50
|
+
case "result":
|
|
51
|
+
if (!parsed.patch || typeof parsed.patch !== "object") throw new Error("malformed result patch");
|
|
52
|
+
return parsed as ProducerChildMessage;
|
|
53
|
+
case "error":
|
|
54
|
+
if (typeof parsed.code !== "string" || typeof parsed.message !== "string") {
|
|
55
|
+
throw new Error("malformed error message");
|
|
56
|
+
}
|
|
57
|
+
return parsed as ProducerChildMessage;
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`unknown protocol type: ${(parsed as { type: string }).type}`);
|
|
60
|
+
}
|
|
61
|
+
}
|