codex-orchestrator 2.0.5 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +1 -0
- package/dist/src/v2/acceptance-proof.d.ts +7 -0
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +111 -5
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/command.d.ts +2 -0
- package/dist/src/v2/adapters/command.d.ts.map +1 -1
- package/dist/src/v2/adapters/command.js +67 -8
- package/dist/src/v2/adapters/command.js.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.js +16 -5
- package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
- package/dist/src/v2/android-proof-runner.d.ts +93 -0
- package/dist/src/v2/android-proof-runner.d.ts.map +1 -0
- package/dist/src/v2/android-proof-runner.js +921 -0
- package/dist/src/v2/android-proof-runner.js.map +1 -0
- package/dist/src/v2/config.d.ts +13 -0
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +57 -1
- package/dist/src/v2/config.js.map +1 -1
- package/dist/src/v2/immutable-workflow-publisher.js +6 -1
- package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
- package/dist/src/v2/mobile-lease.d.ts +9 -0
- package/dist/src/v2/mobile-lease.d.ts.map +1 -1
- package/dist/src/v2/mobile-lease.js +27 -3
- package/dist/src/v2/mobile-lease.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +4 -0
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +156 -44
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +3 -0
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +12 -1
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +63 -10
- package/dist/src/v2/runtime.js.map +1 -1
- package/docs/deep-dive.md +10 -2
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/skills/acceptance-proof/SKILL.md +1 -1
- package/internal-workflow/skills/acceptance-proof/references/android.md +6 -6
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +10 -8
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +17 -3
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +30 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +30 -6
- package/package.json +1 -1
- package/internal-workflow/skills/acceptance-proof/tools/android-lease.mjs +0 -280
|
@@ -43,15 +43,27 @@ A standalone reviewer performs one bounded Full over all applicable lenses and
|
|
|
43
43
|
returns only `Approved | Needs Work | Rejected`; it does not invent owner state
|
|
44
44
|
or claim Closure.
|
|
45
45
|
|
|
46
|
+
## Minimum Solution First
|
|
47
|
+
|
|
48
|
+
Begin every Full review with the evidence-backed scope delta, before checking whether the proposed implementation is detailed enough:
|
|
49
|
+
|
|
50
|
+
1. Identify behavior already implemented and require preservation/regression proof instead of reimplementation.
|
|
51
|
+
2. Challenge every new endpoint, service, durable state, configuration input, schema/public contract, repository, and data owner against an existing owner or seam.
|
|
52
|
+
3. Require one approved requirement or concrete failure path for each surviving mechanism. If deletion still satisfies behavior, invariants, and proof, report the mechanism as excess.
|
|
53
|
+
4. Treat invented product values, copy, eligibility policy, thresholds, and defaults as authority gaps when they shape observable behavior; detailed implementation does not resolve them.
|
|
54
|
+
|
|
55
|
+
Prefer the smallest repair in this order: delete excess, reuse an existing owner/seam, narrowly extend an existing contract, then add a new mechanism only when the earlier options cannot satisfy a named invariant.
|
|
56
|
+
|
|
46
57
|
## Review Lenses
|
|
47
58
|
|
|
48
59
|
Scale depth to the profile and inspect only applicable lenses:
|
|
49
60
|
|
|
50
61
|
- **Determinism and evidence:** execution-critical paths, symbols, commands,
|
|
51
62
|
contracts, fixtures, and claims are confirmed rather than invented.
|
|
52
|
-
- **Scope and minimum solution:** the spec preserves approved scope,
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
- **Scope and minimum solution:** the spec preserves approved scope,
|
|
64
|
+
distinguishes `preserve + regression proof` from new work, uses existing
|
|
65
|
+
owners/seams, and ties every added mechanism to a requirement or concrete
|
|
66
|
+
failure path.
|
|
55
67
|
- **Sequencing and ownership:** phases are safe, sources of truth are explicit
|
|
56
68
|
where drift is possible, and multi-agent write scopes are disjoint.
|
|
57
69
|
- **Validation:** each behavior has an observable proof; contract-risk work maps
|
|
@@ -81,6 +93,8 @@ Prefer deleting or narrowing an unsafe proposal before adding flags, telemetry,
|
|
|
81
93
|
fallbacks, compatibility paths, or rollout machinery. Optional improvements
|
|
82
94
|
remain optional unless source authority approves them.
|
|
83
95
|
|
|
96
|
+
Do not approve a duplicate public seam merely because it is internally consistent. Do not ask for more detailed recovery, configuration, analytics, or compatibility machinery until the mechanism itself passes the deletion challenge.
|
|
97
|
+
|
|
84
98
|
## Defects And Decision
|
|
85
99
|
|
|
86
100
|
- **Blocker:** unsafe or impossible to execute as written.
|
|
@@ -19,6 +19,36 @@
|
|
|
19
19
|
"prompt": "An approved spec receives a substantive execution change after review.",
|
|
20
20
|
"expected": ["invalidate approval for the changed revision", "review only invalidated coverage"],
|
|
21
21
|
"forbidden": ["execute under stale approval", "restart unrelated coverage"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "artifact-existing-seam-extension",
|
|
25
|
+
"prompt": "A spec proposes a second public access endpoint and new client loader although the existing versioned endpoint can accept optional fields without breaking callers.",
|
|
26
|
+
"expected": ["report excess scope", "prefer a narrow extension of the existing endpoint and loader"],
|
|
27
|
+
"forbidden": ["approve the duplicate seam because its contract is detailed", "add compatibility machinery for the duplicate endpoint"]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "artifact-already-implemented-preservation",
|
|
31
|
+
"prompt": "Repository evidence proves the target screen already displays the current plan and remaining time, but the spec lists both as new implementation work.",
|
|
32
|
+
"expected": ["reclassify existing behavior as preserve plus regression proof", "limit implementation to the actual remaining delta"],
|
|
33
|
+
"forbidden": ["plan a second implementation", "ignore current code evidence"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "artifact-invented-product-policy",
|
|
37
|
+
"prompt": "The source requires configurable lookback and cooldown periods but supplies no values; the spec invents defaults and builds configuration, validation, and analytics around them.",
|
|
38
|
+
"expected": ["treat material values as an authority gap", "block before expanding the technical contract"],
|
|
39
|
+
"forbidden": ["approve invented defaults", "reward extra implementation detail as determinism"]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "artifact-closure-complexity-guard",
|
|
43
|
+
"prompt": "A repair for one failure-contract defect adds a service, durable state machine, configuration input, and public DTO. Review the repair in Closure.",
|
|
44
|
+
"expected": ["verify the supplied defect", "challenge each added mechanism against existing seams", "report scope change without starting a separate simplification review"],
|
|
45
|
+
"forbidden": ["verify only the old defect ID", "automatically launch a fresh Full"]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "artifact-local-repair-review-budget",
|
|
49
|
+
"prompt": "A consolidated repair changes only wording and one existing validation command while preserving scope, owners, public contracts, and mandatory-lens coverage.",
|
|
50
|
+
"expected": ["use coordinator verification for ordinary findings", "reuse valid Full coverage"],
|
|
51
|
+
"forbidden": ["launch Closure for ordinary findings", "launch a fresh Full because the revision changed"]
|
|
22
52
|
}
|
|
23
53
|
]
|
|
24
54
|
}
|
|
@@ -31,6 +31,8 @@ spec revision, and mandatory external evidence. Save a useful blocked spec when
|
|
|
31
31
|
a product or contract decision is missing; do not launch review to discover a
|
|
32
32
|
known authority gap.
|
|
33
33
|
|
|
34
|
+
Require an evidence-backed scope delta before review: approved behavior, current capability/owner/seam, and the smallest remaining implementation delta for each material requirement. Already implemented behavior is preservation/regression scope. Unresolved product values, copy, policy, thresholds, or ownership choices that materially shape behavior block preflight instead of receiving invented defaults.
|
|
35
|
+
|
|
34
36
|
`medium` is the default. Use:
|
|
35
37
|
|
|
36
38
|
- `simple` for one narrow owner with direct proof and no material uncertainty;
|
|
@@ -50,9 +52,12 @@ authorize flags, telemetry, compatibility paths, generic fallbacks, or rollout
|
|
|
50
52
|
machinery unless the source or a concrete failure requires them.
|
|
51
53
|
|
|
52
54
|
Give each reviewer a bounded capsule containing the current spec, authority,
|
|
53
|
-
approved scope,
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
approved scope, scope delta, current owners/seams, evidence, review question,
|
|
56
|
+
assigned lenses, and current defect records. Name behavior that already exists
|
|
57
|
+
and the justification for every proposed new endpoint, service, durable state,
|
|
58
|
+
configuration input, schema/public contract, repository, or data owner. For
|
|
59
|
+
Closure also include the repaired sections, affected contracts, and repair
|
|
60
|
+
complexity delta. Do not pass raw parent history or unrelated inventories.
|
|
56
61
|
|
|
57
62
|
## Topology
|
|
58
63
|
|
|
@@ -63,13 +68,32 @@ Do not pass raw parent history or unrelated inventories.
|
|
|
63
68
|
|
|
64
69
|
Root launches and aggregates reviewers. A reviewer child runs the
|
|
65
70
|
`implementation-spec-review` Adapter inline and never spawns another reviewer.
|
|
66
|
-
Reuse valid coverage for the same revision and question.
|
|
71
|
+
Reuse valid coverage for the same revision and question. Parallel high-profile
|
|
72
|
+
reviewers are one Full review round, not sequential rounds.
|
|
67
73
|
|
|
68
74
|
After one consolidated repair, coordinator verification is enough for ordinary
|
|
69
75
|
medium/low findings. Use shared-protocol Closure only for critical/high defects,
|
|
70
76
|
protected trust/data/concurrency/shared-contract impact, or invalidated
|
|
71
|
-
mandatory coverage.
|
|
72
|
-
|
|
77
|
+
mandatory coverage.
|
|
78
|
+
|
|
79
|
+
The default budget is one Full round, one consolidated repair, and at most one
|
|
80
|
+
Closure round. Closure verifies its supplied defects and runs a bounded
|
|
81
|
+
complexity guard over the repair delta:
|
|
82
|
+
|
|
83
|
+
1. Did the repair add a new integration boundary or durable mechanism?
|
|
84
|
+
2. Can it be removed or replaced by an existing owner/seam?
|
|
85
|
+
3. Did it change approved scope?
|
|
86
|
+
|
|
87
|
+
This guard is part of Closure, not a separate simplification review. A further
|
|
88
|
+
targeted Closure is exceptional and requires a newly introduced critical/high
|
|
89
|
+
defect plus materially changed target or evidence; otherwise coordinator
|
|
90
|
+
verification or the shared no-progress/blocked outcome applies.
|
|
91
|
+
|
|
92
|
+
Start a fresh Full only when existing mandatory coverage is invalidated by a
|
|
93
|
+
changed source decision or approved scope, replacement of the primary solution
|
|
94
|
+
or owner, addition of a repository/data owner, or a new public API or durable
|
|
95
|
+
workflow that changes the reviewed architecture. A large diff or accumulated
|
|
96
|
+
clarifications alone do not trigger Full.
|
|
73
97
|
|
|
74
98
|
## Approval
|
|
75
99
|
|
package/package.json
CHANGED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
6
|
-
import { dirname, join, resolve } from 'node:path';
|
|
7
|
-
import { promisify } from 'node:util';
|
|
8
|
-
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
const LEASE_FILE = 'android.json';
|
|
11
|
-
const LEASE_MS = 15 * 60 * 1000;
|
|
12
|
-
|
|
13
|
-
class Blocked extends Error {}
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const [command, ...argv] = process.argv.slice(2);
|
|
17
|
-
const args = parseArgs(argv);
|
|
18
|
-
const result = await execute(command, args);
|
|
19
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
20
|
-
} catch (error) {
|
|
21
|
-
const blocked = error instanceof Blocked;
|
|
22
|
-
process.stdout.write(`${JSON.stringify({
|
|
23
|
-
status: blocked ? 'blocked' : 'internal-error',
|
|
24
|
-
reason: blocked ? error.message : 'Android lease helper failed internally.',
|
|
25
|
-
})}\n`);
|
|
26
|
-
process.exitCode = blocked ? 20 : 70;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function execute(command, args) {
|
|
30
|
-
const leaseRoot = requiredPath(args['lease-root'], 'lease root');
|
|
31
|
-
const artifactPath = requiredPath(args.artifact, 'lease artifact');
|
|
32
|
-
const proofId = requiredString(args['proof-id'], 'proof ID');
|
|
33
|
-
const adb = args.adb
|
|
34
|
-
? requiredPath(args.adb, 'adb path')
|
|
35
|
-
: process.env.ANDROID_ADB || join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools', 'adb');
|
|
36
|
-
const now = parseNow();
|
|
37
|
-
await ensureDirectDirectory(leaseRoot);
|
|
38
|
-
await ensureDirectDirectory(dirname(artifactPath));
|
|
39
|
-
const leasePath = join(leaseRoot, LEASE_FILE);
|
|
40
|
-
|
|
41
|
-
if (command === 'acquire') {
|
|
42
|
-
const appId = requiredAppId(args['app-id']);
|
|
43
|
-
const ownerPid = requiredPid(args['owner-pid']);
|
|
44
|
-
const target = await selectEmulator(adb);
|
|
45
|
-
const existing = await readLease(leasePath);
|
|
46
|
-
if (existing) {
|
|
47
|
-
if (existing.proofId === proofId && existing.appId === appId && existing.ownerPid === ownerPid) {
|
|
48
|
-
await verifyTarget(adb, existing, false);
|
|
49
|
-
return publicResult('reused', existing);
|
|
50
|
-
}
|
|
51
|
-
if (!(Date.parse(existing.expiresAt) < now.getTime() && !pidAlive(existing.ownerPid))) {
|
|
52
|
-
throw new Blocked('An active foreign Android lease already exists.');
|
|
53
|
-
}
|
|
54
|
-
if (existing.serial !== target.serial || existing.appId !== appId) {
|
|
55
|
-
throw new Blocked('A stale Android lease does not match the requested target identity.');
|
|
56
|
-
}
|
|
57
|
-
await rm(leasePath);
|
|
58
|
-
}
|
|
59
|
-
const livePid = await appPid(adb, target.serial, appId);
|
|
60
|
-
if (livePid !== undefined) throw new Blocked('The requested Android app already has a user-owned live process.');
|
|
61
|
-
const lease = {
|
|
62
|
-
schema: 'codex-orchestrator.android-lease',
|
|
63
|
-
version: 1,
|
|
64
|
-
status: 'active',
|
|
65
|
-
proofId,
|
|
66
|
-
token: randomUUID(),
|
|
67
|
-
serial: target.serial,
|
|
68
|
-
appId,
|
|
69
|
-
ownerPid,
|
|
70
|
-
appPid: null,
|
|
71
|
-
acquiredAt: now.toISOString(),
|
|
72
|
-
expiresAt: new Date(now.getTime() + LEASE_MS).toISOString(),
|
|
73
|
-
updatedAt: now.toISOString(),
|
|
74
|
-
};
|
|
75
|
-
await createExclusiveJson(leasePath, lease);
|
|
76
|
-
await writeAtomicJson(artifactPath, lease);
|
|
77
|
-
return publicResult('acquired', lease);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const token = requiredString(args.token, 'lease token');
|
|
81
|
-
const lease = await requireOwnedLease(leasePath, proofId, token, now);
|
|
82
|
-
if (command === 'bind') {
|
|
83
|
-
await verifyTarget(adb, lease, false, true);
|
|
84
|
-
const pid = await appPid(adb, lease.serial, lease.appId);
|
|
85
|
-
if (pid === undefined) throw new Blocked('The leased Android app is not running.');
|
|
86
|
-
const bound = { ...lease, appPid: pid, updatedAt: now.toISOString() };
|
|
87
|
-
await replaceJson(leasePath, bound);
|
|
88
|
-
await writeAtomicJson(artifactPath, bound);
|
|
89
|
-
return publicResult('bound', bound);
|
|
90
|
-
}
|
|
91
|
-
if (command === 'verify') {
|
|
92
|
-
await verifyTarget(adb, lease, true);
|
|
93
|
-
await writeAtomicJson(artifactPath, { ...lease, updatedAt: now.toISOString() });
|
|
94
|
-
return publicResult('verified', lease);
|
|
95
|
-
}
|
|
96
|
-
if (command === 'release') {
|
|
97
|
-
await verifyTarget(adb, lease, Number.isSafeInteger(lease.appPid));
|
|
98
|
-
const released = { ...lease, status: 'released', updatedAt: now.toISOString() };
|
|
99
|
-
await writeAtomicJson(artifactPath, released);
|
|
100
|
-
await rm(leasePath);
|
|
101
|
-
await syncDirectory(dirname(leasePath));
|
|
102
|
-
return publicResult('released', released);
|
|
103
|
-
}
|
|
104
|
-
throw new Blocked('Unknown Android lease command.');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function selectEmulator(adb) {
|
|
108
|
-
const output = await adbCommand(adb, ['devices', '-l']);
|
|
109
|
-
const rows = output.stdout.split(/\r?\n/u).slice(1).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
110
|
-
const [serial, state] = line.split(/\s+/u);
|
|
111
|
-
return { serial, state };
|
|
112
|
-
});
|
|
113
|
-
if (rows.some((row) => !row.serial.startsWith('emulator-'))) throw new Blocked('Physical Android devices are not eligible for runner leases.');
|
|
114
|
-
if (rows.length !== 1 || rows[0].state !== 'device') throw new Blocked('Exactly one online Android emulator is required.');
|
|
115
|
-
const boot = await adbCommand(adb, ['-s', rows[0].serial, 'shell', 'getprop', 'sys.boot_completed']);
|
|
116
|
-
if (boot.stdout.trim() !== '1') throw new Blocked('The Android emulator has not completed boot.');
|
|
117
|
-
return rows[0];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function verifyTarget(adb, lease, requireBoundApp, allowUnboundLive = false) {
|
|
121
|
-
const target = await selectEmulator(adb);
|
|
122
|
-
if (target.serial !== lease.serial) throw new Blocked('The leased Android emulator identity changed.');
|
|
123
|
-
const currentPid = await appPid(adb, lease.serial, lease.appId);
|
|
124
|
-
if (requireBoundApp) {
|
|
125
|
-
if (!Number.isSafeInteger(lease.appPid) || currentPid !== lease.appPid) {
|
|
126
|
-
throw new Blocked('The leased Android app PID changed or disappeared.');
|
|
127
|
-
}
|
|
128
|
-
} else if (!allowUnboundLive && lease.appPid === null && currentPid !== undefined) {
|
|
129
|
-
throw new Blocked('The leased Android app became live before runner binding.');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function appPid(adb, serial, appId) {
|
|
134
|
-
const result = await adbCommand(adb, ['-s', serial, 'shell', 'pidof', '-s', appId], true);
|
|
135
|
-
const value = result.stdout.trim();
|
|
136
|
-
if (!value) return undefined;
|
|
137
|
-
const pid = Number(value);
|
|
138
|
-
if (!Number.isSafeInteger(pid) || pid < 1) throw new Blocked('Android app PID observation is invalid.');
|
|
139
|
-
return pid;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function adbCommand(adb, args, allowFailure = false) {
|
|
143
|
-
try {
|
|
144
|
-
return await execFileAsync(adb, args, { timeout: 15_000, maxBuffer: 1024 * 1024, env: process.env });
|
|
145
|
-
} catch (error) {
|
|
146
|
-
if (allowFailure && typeof error === 'object' && error !== null && 'stdout' in error) {
|
|
147
|
-
return { stdout: String(error.stdout || ''), stderr: String(error.stderr || '') };
|
|
148
|
-
}
|
|
149
|
-
throw new Blocked('Android tooling did not return a usable target observation.');
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function parseArgs(argv) {
|
|
154
|
-
const result = {};
|
|
155
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
156
|
-
const arg = argv[index];
|
|
157
|
-
if (!arg.startsWith('--')) throw new Blocked('Android lease arguments are invalid.');
|
|
158
|
-
const equals = arg.indexOf('=');
|
|
159
|
-
if (equals > 2) {
|
|
160
|
-
result[arg.slice(2, equals)] = arg.slice(equals + 1);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const value = argv[++index];
|
|
164
|
-
if (!value || value.startsWith('--')) throw new Blocked('Android lease argument value is missing.');
|
|
165
|
-
result[arg.slice(2)] = value;
|
|
166
|
-
}
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function parseNow() {
|
|
171
|
-
const value = process.env.CODEX_ORCHESTRATOR_NOW || new Date().toISOString();
|
|
172
|
-
const date = new Date(value);
|
|
173
|
-
if (Number.isNaN(date.getTime()) || date.toISOString() !== value) throw new Error('invalid lease clock');
|
|
174
|
-
return date;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function requireOwnedLease(path, proofId, token, now) {
|
|
178
|
-
const lease = await readLease(path);
|
|
179
|
-
if (!lease || lease.proofId !== proofId || lease.token !== token) throw new Blocked('Android lease ownership does not match.');
|
|
180
|
-
if (lease.status !== 'active' || Date.parse(lease.expiresAt) < now.getTime()) throw new Blocked('Android lease is not active.');
|
|
181
|
-
return lease;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function readLease(path) {
|
|
185
|
-
try {
|
|
186
|
-
return validateLease(JSON.parse(await readFile(path, 'utf8')));
|
|
187
|
-
} catch (error) {
|
|
188
|
-
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') return undefined;
|
|
189
|
-
throw new Blocked('Android lease state is malformed.');
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function validateLease(value) {
|
|
194
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid lease');
|
|
195
|
-
const keys = ['schema', 'version', 'status', 'proofId', 'token', 'serial', 'appId', 'ownerPid', 'appPid', 'acquiredAt', 'expiresAt', 'updatedAt'].sort();
|
|
196
|
-
const actual = Object.keys(value).sort();
|
|
197
|
-
if (actual.length !== keys.length || actual.some((key, index) => key !== keys[index])) throw new Error('invalid lease keys');
|
|
198
|
-
if (value.schema !== 'codex-orchestrator.android-lease' || value.version !== 1 || !['active', 'released'].includes(value.status)) throw new Error('invalid lease schema');
|
|
199
|
-
requiredString(value.proofId, 'proof ID');
|
|
200
|
-
requiredString(value.token, 'token');
|
|
201
|
-
if (!/^emulator-[0-9]+$/u.test(value.serial)) throw new Error('invalid serial');
|
|
202
|
-
requiredAppId(value.appId);
|
|
203
|
-
requiredPid(value.ownerPid);
|
|
204
|
-
if (value.appPid !== null) requiredPid(value.appPid);
|
|
205
|
-
for (const field of ['acquiredAt', 'expiresAt', 'updatedAt']) {
|
|
206
|
-
if (new Date(value[field]).toISOString() !== value[field]) throw new Error('invalid lease timestamp');
|
|
207
|
-
}
|
|
208
|
-
return value;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function ensureDirectDirectory(path) {
|
|
212
|
-
const absolute = resolve(path);
|
|
213
|
-
await mkdir(absolute, { recursive: true, mode: 0o700 });
|
|
214
|
-
const info = await lstat(absolute);
|
|
215
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('unsafe lease directory');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function createExclusiveJson(path, value) {
|
|
219
|
-
const handle = await open(path, 'wx', 0o600);
|
|
220
|
-
try {
|
|
221
|
-
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
222
|
-
await handle.sync();
|
|
223
|
-
} finally {
|
|
224
|
-
await handle.close();
|
|
225
|
-
}
|
|
226
|
-
await syncDirectory(dirname(path));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function replaceJson(path, value) {
|
|
230
|
-
await writeAtomicJson(path, value);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function writeAtomicJson(path, value) {
|
|
234
|
-
const temp = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
235
|
-
const handle = await open(temp, 'wx', 0o600);
|
|
236
|
-
try {
|
|
237
|
-
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
238
|
-
await handle.sync();
|
|
239
|
-
} finally {
|
|
240
|
-
await handle.close();
|
|
241
|
-
}
|
|
242
|
-
await rename(temp, path);
|
|
243
|
-
await syncDirectory(dirname(path));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async function syncDirectory(path) {
|
|
247
|
-
const handle = await open(path, 'r');
|
|
248
|
-
try { await handle.sync(); } finally { await handle.close(); }
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function publicResult(status, lease) {
|
|
252
|
-
return { status, proofId: lease.proofId, token: lease.token, serial: lease.serial, appId: lease.appId, appPid: lease.appPid, expiresAt: lease.expiresAt };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function requiredPath(value, field) {
|
|
256
|
-
const text = requiredString(value, field);
|
|
257
|
-
if (!text.startsWith('/')) throw new Blocked(`${field} must be absolute.`);
|
|
258
|
-
return resolve(text);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function requiredString(value, field) {
|
|
262
|
-
if (typeof value !== 'string' || value.length === 0 || value.length > 4096) throw new Blocked(`${field} is invalid.`);
|
|
263
|
-
return value;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function requiredAppId(value) {
|
|
267
|
-
const text = requiredString(value, 'app ID');
|
|
268
|
-
if (!/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u.test(text)) throw new Blocked('Android app ID is invalid.');
|
|
269
|
-
return text;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function requiredPid(value) {
|
|
273
|
-
const pid = Number(value);
|
|
274
|
-
if (!Number.isSafeInteger(pid) || pid < 1) throw new Blocked('Android owner PID is invalid.');
|
|
275
|
-
return pid;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function pidAlive(pid) {
|
|
279
|
-
try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
|
|
280
|
-
}
|