@fitlab-ai/agent-infra 0.9.6 → 0.9.7

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.
Files changed (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,106 @@
1
+ import { runCodexSandboxController, verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
2
+ const USAGE = 'Usage: agent-infra-internal codex-sandbox-controller <run|verify-context> [options]\n';
3
+ function fail(code, message, exitCode = 1) {
4
+ process.stdout.write(`${JSON.stringify({
5
+ status: 'failed',
6
+ changed: false,
7
+ error: { code, message }
8
+ })}\n`);
9
+ process.exitCode = exitCode;
10
+ }
11
+ function parse(args) {
12
+ if (args[0] === '--help' || args[0] === '-h') {
13
+ process.stdout.write(USAGE);
14
+ return null;
15
+ }
16
+ const operation = args[0];
17
+ if (!operation || !['run', 'verify-context'].includes(operation)) {
18
+ process.stderr.write(USAGE);
19
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', 'operation must be run or verify-context', 2);
20
+ return null;
21
+ }
22
+ const allowed = new Set([
23
+ '--task-id',
24
+ '--task-ref',
25
+ '--context',
26
+ '--executor-model',
27
+ '--executor-reasoning-effort',
28
+ '--reviewer-model',
29
+ '--reviewer-reasoning-effort'
30
+ ]);
31
+ const values = {};
32
+ for (let index = 1; index < args.length; index += 1) {
33
+ const flag = args[index];
34
+ const value = args[++index];
35
+ if (!allowed.has(flag) || Object.hasOwn(values, flag) || !value || value.startsWith('--')) {
36
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', `invalid option '${flag}'`, 2);
37
+ return null;
38
+ }
39
+ values[flag] = value;
40
+ }
41
+ return { operation, values };
42
+ }
43
+ async function codexSandboxController(args = []) {
44
+ const parsed = parse(args);
45
+ if (!parsed || process.exitCode)
46
+ return;
47
+ const taskId = parsed.values['--task-id'];
48
+ if (!taskId) {
49
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', '--task-id is required', 2);
50
+ return;
51
+ }
52
+ try {
53
+ if (parsed.operation === 'verify-context') {
54
+ const contextPath = parsed.values['--context']
55
+ ?? process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
56
+ if (!contextPath)
57
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_MISSING');
58
+ const context = verifyCodexSandboxControllerContext(contextPath, taskId);
59
+ process.stdout.write(`${JSON.stringify({
60
+ status: 'ready',
61
+ changed: false,
62
+ context: {
63
+ taskId: context.taskId,
64
+ controlGeneration: context.controlGeneration,
65
+ controllerInstanceDigest: context.controllerInstanceDigest,
66
+ expiresAt: context.expiresAt,
67
+ buildIdentity: context.buildIdentity,
68
+ hookDefinitionHash: context.hookDefinitionHash,
69
+ lifecycleProfilesHash: context.lifecycleProfilesHash
70
+ },
71
+ error: null
72
+ })}\n`);
73
+ return;
74
+ }
75
+ const taskRef = parsed.values['--task-ref'];
76
+ if (!taskRef)
77
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_REF_REQUIRED');
78
+ const policy = [
79
+ '--executor-model',
80
+ '--executor-reasoning-effort',
81
+ '--reviewer-model',
82
+ '--reviewer-reasoning-effort'
83
+ ];
84
+ if (policy.some((flag) => parsed.values[flag] !== undefined)
85
+ && policy.some((flag) => parsed.values[flag] === undefined)) {
86
+ throw new Error('CODEX_SANDBOX_CONTROLLER_MODEL_POLICY_INVALID');
87
+ }
88
+ const exitCode = await runCodexSandboxController({
89
+ taskId,
90
+ taskRef,
91
+ executorModel: parsed.values['--executor-model'],
92
+ executorReasoningEffort: parsed.values['--executor-reasoning-effort'],
93
+ reviewerModel: parsed.values['--reviewer-model'],
94
+ reviewerReasoningEffort: parsed.values['--reviewer-reasoning-effort']
95
+ });
96
+ process.exitCode = exitCode;
97
+ }
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1]
101
+ ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
102
+ fail(code, message);
103
+ }
104
+ }
105
+ export { codexSandboxController };
106
+ //# sourceMappingURL=codex-sandbox-controller.js.map
@@ -1,8 +1,8 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
2
- import { abortCommitIntent, activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, completeCommitIntent, pauseOrchestration, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt } from '../task/orchestration.js';
2
+ import { abortCommitIntent, activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, awaitOrchestrationDelegationActivation, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, completeCommitIntent, dispatchOrchestrationDelegation, pauseOrchestration, recoverCommitIntent, recoverPreparedOrchestrationDelegation, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt } from '../task/orchestration.js';
3
3
  import { prepareCodexOrchestrationDelegation } from '../task/codex-orchestration.js';
4
4
  import { isAgentClientId } from '../agent-clients/types.js';
5
- const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-start|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-terminate|commit-status> [options]\n';
5
+ const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|dispatch|await-activation|recover-prepared|hook-start|hook-stop|advance|pause|status|commit-start|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-terminate|commit-status> [options]\n';
6
6
  function usageFailure(message) {
7
7
  process.stdout.write(`${JSON.stringify({
8
8
  status: 'failed', changed: false,
@@ -22,7 +22,8 @@ async function taskOrchestration(args = []) {
22
22
  }
23
23
  const [taskRef, intent] = args;
24
24
  if (![
25
- 'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
25
+ 'begin-or-resume', 'route', 'prepare', 'dispatch', 'await-activation', 'recover-prepared',
26
+ 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
26
27
  'commit-start', 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-terminate', 'commit-status'
27
28
  ].includes(intent)) {
28
29
  usageFailure(`unknown intent '${intent}'`);
@@ -45,7 +46,9 @@ async function taskOrchestration(args = []) {
45
46
  if (![
46
47
  '--max-steps', '--executor-model', '--executor-reasoning-effort',
47
48
  '--reviewer-model', '--reviewer-reasoning-effort',
48
- '--client', '--requested-model', '--requested-reasoning-effort', '--parent-id', '--before-fingerprint',
49
+ '--client', '--requested-model', '--requested-reasoning-effort', '--capability-token',
50
+ '--parent-id', '--before-fingerprint',
51
+ '--stage', '--round', '--artifact', '--role',
49
52
  '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
50
53
  '--model-fallback-reason', '--reasoning-effort-fallback-reason',
51
54
  '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
@@ -264,9 +267,38 @@ async function taskOrchestration(args = []) {
264
267
  result = await prepareCodexOrchestrationDelegation(taskRef, {
265
268
  client: values['--client'],
266
269
  requestedModel: values['--requested-model'],
267
- requestedReasoningEffort: values['--requested-reasoning-effort']
270
+ requestedReasoningEffort: values['--requested-reasoning-effort'],
271
+ capabilityToken: values['--capability-token']
268
272
  }, { orchestrationOptions: coreOptions });
269
273
  }
274
+ else if (intent === 'dispatch') {
275
+ result = dispatchOrchestrationDelegation(taskRef, coreOptions);
276
+ }
277
+ else if (intent === 'await-activation') {
278
+ const missing = requireValues(['--stage', '--round', '--artifact', '--role']);
279
+ if (missing) {
280
+ usageFailure(`intent 'await-activation' requires '${missing}'`);
281
+ return;
282
+ }
283
+ const round = Number(values['--round']);
284
+ if (!Number.isSafeInteger(round) || round < 1) {
285
+ usageFailure('--round must be a positive integer');
286
+ return;
287
+ }
288
+ if (!['executor', 'reviewer'].includes(values['--role'])) {
289
+ usageFailure('--role must be executor or reviewer');
290
+ return;
291
+ }
292
+ result = await awaitOrchestrationDelegationActivation(taskRef, {
293
+ stage: values['--stage'],
294
+ round,
295
+ artifact: values['--artifact'],
296
+ role: values['--role']
297
+ }, coreOptions);
298
+ }
299
+ else if (intent === 'recover-prepared') {
300
+ result = recoverPreparedOrchestrationDelegation(taskRef, coreOptions);
301
+ }
270
302
  else if (intent === 'hook-start') {
271
303
  const missing = requireValues([
272
304
  ...(taskRef === 'auto' ? ['--client'] : []),
@@ -5,6 +5,7 @@ import semver from 'semver';
5
5
  import { getPlatformAdapter, registerPlatformAdapter } from './adapters.js';
6
6
  import { createGitHubClient, MINIMUM_GITHUB_CLI_VERSION } from './github-client.js';
7
7
  import { platformResult } from './types.js';
8
+ const CURRENT_USER_QUERY = 'query { viewer { login } }';
8
9
  function findRepoRoot(cwd) {
9
10
  try {
10
11
  return execFileSync('git', ['rev-parse', '--show-toplevel'], {
@@ -89,18 +90,19 @@ function resolveGitHubContext(options) {
89
90
  error: { code: 'UPSTREAM_UNRESOLVED', message: 'Unable to resolve the upstream repository', retryable: false }
90
91
  });
91
92
  }
92
- const user = client.json(['api', 'user'], { cwd });
93
+ const user = client.json(['api', 'graphql', '-f', `query=${CURRENT_USER_QUERY}`], { cwd });
93
94
  if (!user.ok)
94
95
  return failure(user.error, upstream);
95
96
  const userValue = user.value;
97
+ const currentUser = userValue?.data?.viewer?.login || null;
96
98
  const permissions = client.json(['api', `repos/${upstream}`], { cwd });
97
99
  if (!permissions.ok)
98
100
  return failure(permissions.error, upstream);
99
101
  const permissionValue = permissions.value;
100
102
  const values = permissionValue?.permissions || {};
101
103
  const capabilities = {
102
- authenticated: Boolean(userValue?.login),
103
- comment: Boolean(userValue?.login),
104
+ authenticated: Boolean(currentUser),
105
+ comment: Boolean(currentUser),
104
106
  triage: Boolean(values.triage || values.push || values.admin),
105
107
  push: Boolean(values.push || values.admin),
106
108
  admin: Boolean(values.admin)
@@ -108,7 +110,7 @@ function resolveGitHubContext(options) {
108
110
  const status = Object.values(capabilities).every(Boolean) ? 'no-op' : 'degraded';
109
111
  return platformResult(status, {
110
112
  changed: false,
111
- platform: { type: platform, repository: upstream, currentUser: userValue?.login || null },
113
+ platform: { type: platform, repository: upstream, currentUser },
112
114
  resource: { kind: 'repository', number: null },
113
115
  capabilities,
114
116
  operations: [{ name: 'resolve', status: 'no-op', reasonCode: null }]
@@ -1,7 +1,9 @@
1
1
  import process from 'node:process';
2
2
  import spawn from 'cross-spawn';
3
3
  import semver from 'semver';
4
- const MINIMUM_GITHUB_CLI_VERSION = '2.16.0';
4
+ // closingIssuesReferences (introduced in gh 2.72.0) is the highest-versioned gh feature
5
+ // the platform layer depends on; see github-release-notes.ts:102.
6
+ const MINIMUM_GITHUB_CLI_VERSION = '2.72.0';
5
7
  const GITHUB_CLI_MAX_BUFFER = 64 * 1024 * 1024;
6
8
  const ERROR_DETAIL_LIMIT = 4096;
7
9
  function boundedFailureDetail(result) {
@@ -76,6 +78,41 @@ function classifyGitHubFailure(result) {
76
78
  }
77
79
  return { code: 'PLATFORM_REQUEST_FAILED', message: detail || 'GitHub request failed', retryable: false };
78
80
  }
81
+ function responseUrl(args) {
82
+ const endpoint = args.find((arg, index) => index > 0 && !arg.startsWith('-')) || '';
83
+ return endpoint.startsWith('http') ? endpoint : `https://api.github.com/${endpoint.replace(/^\//, '')}`;
84
+ }
85
+ function parseIncludedResponse(stdout, args) {
86
+ const statusBlocks = [...stdout.matchAll(/^HTTP\/[^\r\n]+/gim)];
87
+ const headerStart = statusBlocks.at(-1)?.index;
88
+ if (headerStart === undefined)
89
+ return null;
90
+ const response = stdout.slice(headerStart);
91
+ const separator = response.match(/\r?\n\r?\n/);
92
+ if (!separator || separator.index === undefined)
93
+ return null;
94
+ const header = response.slice(0, separator.index);
95
+ const body = response.slice(separator.index + separator[0].length);
96
+ const status = header.match(/^HTTP\/[^ ]+\s+(\d{3})/mi)?.[1];
97
+ if (!status)
98
+ return null;
99
+ const date = header.match(/^Date:\s*(.+)$/mi)?.[1]?.trim();
100
+ const links = [...header.matchAll(/^Link:\s*(.+)$/gim)].flatMap((match) => match[1].split(',').map((value) => value.trim()));
101
+ try {
102
+ return {
103
+ value: JSON.parse(body || 'null'),
104
+ metadata: {
105
+ status: Number(status),
106
+ requestUrl: responseUrl(args),
107
+ ...(date ? { date } : {}),
108
+ links
109
+ }
110
+ };
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
79
116
  function createGitHubClient(options = {}) {
80
117
  const runner = options.runner || defaultRunner;
81
118
  const delays = options.retryDelaysMs || retryDelaysFromEnvironment();
@@ -134,8 +171,24 @@ function createGitHubClient(options = {}) {
134
171
  error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub returned invalid JSON', retryable: true }
135
172
  };
136
173
  }
174
+ },
175
+ jsonWithMetadata(args, request = {}) {
176
+ const includeArgs = args[0] === 'api' && args[1] !== '--include'
177
+ ? [args[0], '--include', ...args.slice(1)]
178
+ : args;
179
+ const result = run(includeArgs, request);
180
+ if (!result.ok)
181
+ return result;
182
+ const parsed = parseIncludedResponse(result.value, args);
183
+ if (!parsed) {
184
+ return {
185
+ ok: false,
186
+ error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub response metadata or JSON is invalid', retryable: true }
187
+ };
188
+ }
189
+ return { ok: true, value: parsed };
137
190
  }
138
191
  };
139
192
  }
140
- export { classifyGitHubFailure, createGitHubClient, MINIMUM_GITHUB_CLI_VERSION };
193
+ export { classifyGitHubFailure, createGitHubClient, MINIMUM_GITHUB_CLI_VERSION, parseIncludedResponse };
141
194
  //# sourceMappingURL=github-client.js.map
@@ -0,0 +1,319 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { verifySnapshot } from './store.js';
6
+ function repositoryKey(repository) {
7
+ return crypto.createHash('sha256').update(repository).digest('hex').slice(0, 32);
8
+ }
9
+ function checkpointPaths(root, repository) {
10
+ const directory = path.join(root, 'checkpoints', 'github');
11
+ const base = path.join(directory, repositoryKey(repository));
12
+ return { directory, checkpoint: `${base}.json`, lock: `${base}.lock` };
13
+ }
14
+ function processStartToken(pid) {
15
+ if (process.platform === 'linux') {
16
+ try {
17
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
18
+ const closing = stat.lastIndexOf(')');
19
+ const fields = stat.slice(closing + 2).split(' ');
20
+ return fields[19] ?? null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ return pid === process.pid ? `self:${process.pid}` : null;
27
+ }
28
+ function fsyncDirectory(directory) {
29
+ if (process.platform === 'win32')
30
+ return;
31
+ const fd = fs.openSync(directory, 'r');
32
+ try {
33
+ fs.fsyncSync(fd);
34
+ }
35
+ finally {
36
+ fs.closeSync(fd);
37
+ }
38
+ }
39
+ function ensurePrivateDirectory(directory) {
40
+ const missing = [];
41
+ let current = path.resolve(directory);
42
+ while (!fs.existsSync(current)) {
43
+ missing.push(current);
44
+ const parent = path.dirname(current);
45
+ if (parent === current)
46
+ throw new Error(`cannot create directory tree: ${directory}`);
47
+ current = parent;
48
+ }
49
+ for (const target of missing.reverse()) {
50
+ fs.mkdirSync(target, { mode: 0o700 });
51
+ if (process.platform !== 'win32')
52
+ fs.chmodSync(target, 0o700);
53
+ fsyncDirectory(path.dirname(target));
54
+ }
55
+ }
56
+ function writeTemporary(filePath, value) {
57
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
58
+ const temporary = `${filePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
59
+ const fd = fs.openSync(temporary, 'wx', 0o600);
60
+ try {
61
+ fs.writeFileSync(fd, `${JSON.stringify(value)}\n`);
62
+ fs.fsyncSync(fd);
63
+ }
64
+ finally {
65
+ fs.closeSync(fd);
66
+ }
67
+ return temporary;
68
+ }
69
+ function validCheckpoint(value, repository) {
70
+ if (!value || typeof value !== 'object')
71
+ return false;
72
+ const checkpoint = value;
73
+ return checkpoint.schema === 'github-checkpoint/v1'
74
+ && checkpoint.repository === repository
75
+ && Boolean(checkpoint.snapshotId)
76
+ && Boolean(checkpoint.watermark)
77
+ && Boolean(checkpoint.committedAt)
78
+ && /^[a-f0-9]{64}$/.test(checkpoint.manifestSha256);
79
+ }
80
+ function checkpointMatchesSnapshot(root, value) {
81
+ const verified = verifySnapshot(root, value.snapshotId);
82
+ return verified.ok
83
+ && verified.manifest?.schema === 'raw-manifest/v2'
84
+ && verified.manifest.snapshotId === value.snapshotId
85
+ && verified.manifest.repository === value.repository
86
+ && verified.manifest.manifestSha256 === value.manifestSha256
87
+ && verified.manifest.watermark === value.watermark;
88
+ }
89
+ function checkpointState(value) {
90
+ return { snapshotId: value?.snapshotId ?? null, manifestSha256: value?.manifestSha256 ?? null };
91
+ }
92
+ function readGitHubCheckpoint(root, repository) {
93
+ const target = checkpointPaths(root, repository).checkpoint;
94
+ if (!fs.existsSync(target))
95
+ return { ok: true, value: null };
96
+ try {
97
+ const parsed = JSON.parse(fs.readFileSync(target, 'utf8'));
98
+ const value = {
99
+ schema: parsed.schema,
100
+ repository: parsed.repository,
101
+ snapshotId: parsed.snapshotId,
102
+ watermark: parsed.watermark,
103
+ manifestSha256: parsed.manifestSha256,
104
+ committedAt: parsed.committedAt
105
+ };
106
+ if (!validCheckpoint(value, repository)) {
107
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'GitHub checkpoint schema or identity is invalid' } };
108
+ }
109
+ return { ok: true, value };
110
+ }
111
+ catch {
112
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'GitHub checkpoint is not valid JSON' } };
113
+ }
114
+ }
115
+ function quarantine(lockPath) {
116
+ const target = `${lockPath}.orphaned-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
117
+ fs.renameSync(lockPath, target);
118
+ return target;
119
+ }
120
+ function acquireGitHubCheckpoint(root, repository) {
121
+ const paths = checkpointPaths(root, repository);
122
+ ensurePrivateDirectory(paths.directory);
123
+ const recovery = { recovered: false, quarantined: [] };
124
+ const owner = {
125
+ schema: 'checkpoint-owner/v1',
126
+ ownerId: crypto.randomUUID(),
127
+ pid: process.pid,
128
+ host: os.hostname(),
129
+ processStartToken: processStartToken(process.pid) ?? '',
130
+ createdAt: new Date().toISOString()
131
+ };
132
+ if (!owner.processStartToken) {
133
+ return { ok: false, error: { code: 'CHECKPOINT_TOPOLOGY_UNSUPPORTED', message: 'Process liveness cannot be verified on this host' } };
134
+ }
135
+ let recoveredOwnerId = null;
136
+ if (fs.existsSync(paths.lock)) {
137
+ let previous;
138
+ try {
139
+ previous = JSON.parse(fs.readFileSync(paths.lock, 'utf8'));
140
+ }
141
+ catch {
142
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint lock is unreadable' } };
143
+ }
144
+ if (previous.host !== owner.host || !previous.ownerId || !previous.processStartToken) {
145
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint lock owner topology is unknown' } };
146
+ }
147
+ const currentToken = processStartToken(previous.pid);
148
+ if (currentToken && currentToken === previous.processStartToken) {
149
+ return { ok: false, error: { code: 'CHECKPOINT_BUSY', message: 'Checkpoint is held by a live process' } };
150
+ }
151
+ if (process.platform !== 'linux') {
152
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint owner liveness cannot be proven on this host' } };
153
+ }
154
+ try {
155
+ recoveredOwnerId = previous.ownerId;
156
+ recovery.quarantined.push(quarantine(paths.lock));
157
+ recovery.recovered = true;
158
+ }
159
+ catch {
160
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Unable to quarantine an orphaned checkpoint lock' } };
161
+ }
162
+ }
163
+ try {
164
+ const fd = fs.openSync(paths.lock, 'wx', 0o600);
165
+ try {
166
+ fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`);
167
+ fs.fsyncSync(fd);
168
+ }
169
+ finally {
170
+ fs.closeSync(fd);
171
+ }
172
+ fsyncDirectory(paths.directory);
173
+ }
174
+ catch (error) {
175
+ if (error.code === 'EEXIST') {
176
+ return { ok: false, error: { code: 'CHECKPOINT_BUSY', message: 'Checkpoint lock was acquired by another process' } };
177
+ }
178
+ return { ok: false, error: { code: 'CHECKPOINT_LOCK_FAILED', message: String(error) } };
179
+ }
180
+ const releaseOwnedLock = () => {
181
+ try {
182
+ fs.unlinkSync(paths.lock);
183
+ fsyncDirectory(paths.directory);
184
+ }
185
+ catch {
186
+ // Preserve the original recovery error; a remaining lock will fail closed next time.
187
+ }
188
+ };
189
+ const beforeResult = readGitHubCheckpoint(root, repository);
190
+ if (!beforeResult.ok) {
191
+ releaseOwnedLock();
192
+ return beforeResult;
193
+ }
194
+ let checkpointBefore = beforeResult.value;
195
+ const temporaryCandidates = fs.readdirSync(paths.directory)
196
+ .filter((entry) => entry.startsWith(path.basename(paths.checkpoint) + '.') && entry.endsWith('.tmp'))
197
+ .map((entry) => path.join(paths.directory, entry));
198
+ if (temporaryCandidates.length > 1) {
199
+ releaseOwnedLock();
200
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Multiple checkpoint recovery candidates exist' } };
201
+ }
202
+ if (temporaryCandidates.length === 1) {
203
+ const candidate = temporaryCandidates[0];
204
+ try {
205
+ const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
206
+ const isCandidate = parsed && typeof parsed === 'object' && 'candidateSchema' in parsed
207
+ && parsed.candidateSchema === 'github-checkpoint-candidate/v1';
208
+ if (isCandidate) {
209
+ const candidateValue = parsed;
210
+ const value = candidateValue;
211
+ const expected = checkpointState(checkpointBefore);
212
+ const candidateExpected = candidateValue.expectedOld;
213
+ if (!recoveredOwnerId || candidateValue.ownerId !== recoveredOwnerId) {
214
+ recovery.quarantined.push(quarantine(candidate));
215
+ releaseOwnedLock();
216
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate owner proof does not match the recovered lock owner' } };
217
+ }
218
+ if (!validCheckpoint(value, repository)
219
+ || candidateExpected?.snapshotId !== expected.snapshotId
220
+ || candidateExpected?.manifestSha256 !== expected.manifestSha256) {
221
+ releaseOwnedLock();
222
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate does not match the current head' } };
223
+ }
224
+ if (!checkpointMatchesSnapshot(root, value)) {
225
+ recovery.quarantined.push(quarantine(candidate));
226
+ releaseOwnedLock();
227
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate snapshot failed full verification' } };
228
+ }
229
+ if (process.platform === 'win32' && checkpointBefore) {
230
+ releaseOwnedLock();
231
+ return { ok: false, error: { code: 'CHECKPOINT_TOPOLOGY_UNSUPPORTED', message: 'Checkpoint replacement durability cannot be proven on this host' } };
232
+ }
233
+ fs.renameSync(candidate, paths.checkpoint);
234
+ recovery.recovered = true;
235
+ fsyncDirectory(paths.directory);
236
+ const recovered = readGitHubCheckpoint(root, repository);
237
+ if (!recovered.ok) {
238
+ releaseOwnedLock();
239
+ return recovered;
240
+ }
241
+ checkpointBefore = recovered.value;
242
+ }
243
+ else {
244
+ recovery.quarantined.push(quarantine(candidate));
245
+ releaseOwnedLock();
246
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate lacks owner proof' } };
247
+ }
248
+ }
249
+ catch {
250
+ recovery.quarantined.push(quarantine(candidate));
251
+ }
252
+ }
253
+ return { ok: true, value: { paths, repository, owner, recovery, expectedOld: checkpointBefore } };
254
+ }
255
+ function commitGitHubCheckpoint(lease, value) {
256
+ try {
257
+ const current = JSON.parse(fs.readFileSync(lease.paths.lock, 'utf8'));
258
+ if (current.ownerId !== lease.owner.ownerId) {
259
+ return { ok: false, error: { code: 'CHECKPOINT_OWNER_MISMATCH', message: 'Checkpoint lock ownership changed' } };
260
+ }
261
+ if (value.repository !== lease.repository || !validCheckpoint(value, lease.repository)) {
262
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'Checkpoint value is invalid' } };
263
+ }
264
+ const root = path.dirname(path.dirname(lease.paths.directory));
265
+ if (!checkpointMatchesSnapshot(root, value)) {
266
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'Checkpoint snapshot does not match a verified v2 manifest' } };
267
+ }
268
+ const currentCheckpoint = readGitHubCheckpoint(root, lease.repository);
269
+ if (!currentCheckpoint.ok)
270
+ return currentCheckpoint;
271
+ if (checkpointState(currentCheckpoint.value).snapshotId !== checkpointState(lease.expectedOld).snapshotId
272
+ || checkpointState(currentCheckpoint.value).manifestSha256 !== checkpointState(lease.expectedOld).manifestSha256) {
273
+ return { ok: false, error: { code: 'CHECKPOINT_CONFLICT', message: 'Checkpoint head changed before commit' } };
274
+ }
275
+ const candidate = {
276
+ ...value,
277
+ candidateSchema: 'github-checkpoint-candidate/v1',
278
+ ownerId: lease.owner.ownerId,
279
+ expectedOld: checkpointState(lease.expectedOld)
280
+ };
281
+ const temporary = writeTemporary(lease.paths.checkpoint, candidate);
282
+ const rechecked = readGitHubCheckpoint(root, lease.repository);
283
+ if (!rechecked.ok) {
284
+ fs.unlinkSync(temporary);
285
+ return rechecked;
286
+ }
287
+ if (checkpointState(rechecked.value).snapshotId !== checkpointState(lease.expectedOld).snapshotId
288
+ || checkpointState(rechecked.value).manifestSha256 !== checkpointState(lease.expectedOld).manifestSha256) {
289
+ fs.unlinkSync(temporary);
290
+ return { ok: false, error: { code: 'CHECKPOINT_CONFLICT', message: 'Checkpoint head changed during commit' } };
291
+ }
292
+ if (process.platform === 'win32' && fs.existsSync(lease.paths.checkpoint)) {
293
+ fs.unlinkSync(temporary);
294
+ return { ok: false, error: { code: 'CHECKPOINT_TOPOLOGY_UNSUPPORTED', message: 'Checkpoint replacement durability cannot be proven on this host' } };
295
+ }
296
+ fs.renameSync(temporary, lease.paths.checkpoint);
297
+ if (process.platform !== 'win32')
298
+ fs.chmodSync(lease.paths.checkpoint, 0o600);
299
+ fsyncDirectory(lease.paths.directory);
300
+ return { ok: true, value: undefined };
301
+ }
302
+ catch (error) {
303
+ return { ok: false, error: { code: 'CHECKPOINT_COMMIT_FAILED', message: String(error) } };
304
+ }
305
+ }
306
+ function releaseGitHubCheckpoint(lease) {
307
+ try {
308
+ const current = JSON.parse(fs.readFileSync(lease.paths.lock, 'utf8'));
309
+ if (current.ownerId === lease.owner.ownerId) {
310
+ fs.unlinkSync(lease.paths.lock);
311
+ fsyncDirectory(lease.paths.directory);
312
+ }
313
+ }
314
+ catch {
315
+ // The next invocation will fail closed and report recovery if the lock remains.
316
+ }
317
+ }
318
+ export { acquireGitHubCheckpoint, checkpointPaths, commitGitHubCheckpoint, processStartToken, readGitHubCheckpoint, releaseGitHubCheckpoint };
319
+ //# sourceMappingURL=checkpoint.js.map