@fitlab-ai/agent-infra 0.9.12 → 0.9.13
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 +11 -11
- package/README.zh-CN.md +11 -11
- package/dist/bin/internal-cli.js +24 -19
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +63 -24
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +1 -1
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +119 -104
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +520 -50
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +783 -537
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +11 -12
- package/dist/lib/platform/verification-sync.js +112 -238
- package/dist/lib/pr-review/evidence-grading.js +19 -6
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +33 -5
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-operation.js +12 -1
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/current-contract.js +5 -1
- package/dist/lib/task/decision-details.js +1 -1
- package/dist/lib/task/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +58 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +49 -93
- package/dist/lib/task/pr-delivery-fact.js +102 -101
- package/dist/lib/task/review-fingerprint.js +24 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +43 -16
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/version.js +11 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/init.ts +1 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +47 -22
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +2 -2
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +126 -106
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +560 -53
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +826 -543
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +11 -13
- package/lib/platform/verification-sync.ts +112 -292
- package/lib/pr-review/evidence-grading.ts +25 -8
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +32 -6
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-operation.ts +12 -1
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +6 -2
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +70 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +52 -85
- package/lib/task/pr-delivery-fact.ts +131 -148
- package/lib/task/review-fingerprint.ts +23 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +52 -17
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +1 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
- package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
- package/templates/.agents/rules/pr-sync.en.md +10 -4
- package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
- package/templates/.agents/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +18 -1
- package/templates/.agents/templates/task.zh-CN.md +18 -1
- package/templates/.github/workflows/pr-label.yml +7 -26
package/lib/run/index.ts
CHANGED
|
@@ -38,54 +38,90 @@ export type RunSkillOptions = {
|
|
|
38
38
|
writeStderr?: (chunk: string) => void;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
const USAGE = `Usage: ai run <skill> [task-ref] [
|
|
41
|
+
const USAGE = `Usage: ai run --skill <skill> [--task <task-ref>] [--tui <name>] [--recreate] [-- <skill-args...>]
|
|
42
42
|
|
|
43
43
|
Task skills are scheduled inside the sandbox tmux session; ai run returns once
|
|
44
44
|
the tmux window is created. --recreate authorizes container-only replacement
|
|
45
45
|
when in-place sandbox recovery fails; it is not included in the TUI prompt.
|
|
46
46
|
|
|
47
47
|
Examples:
|
|
48
|
-
ai run create-task "describe the task" --tui codex
|
|
49
|
-
ai run code-task
|
|
48
|
+
ai run --skill create-task "describe the task" --tui codex
|
|
49
|
+
ai run --skill code-task --task 7 --tui codex --recreate`;
|
|
50
50
|
|
|
51
|
-
function
|
|
52
|
-
const
|
|
51
|
+
function optionValue(args: string[], index: number, option: string): string {
|
|
52
|
+
const value = args[index + 1];
|
|
53
|
+
if (!value || value.startsWith('--')) throw new Error(`${option} requires a value`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractHostOptions(args: string[]): {
|
|
58
|
+
positionals: string[];
|
|
59
|
+
forwarded: string[];
|
|
60
|
+
skill: string | null;
|
|
61
|
+
taskRef: string | null;
|
|
62
|
+
tui: string | null;
|
|
63
|
+
recreate: boolean;
|
|
64
|
+
} {
|
|
65
|
+
const positionals: string[] = [];
|
|
66
|
+
const forwarded: string[] = [];
|
|
67
|
+
let skill: string | null = null;
|
|
68
|
+
let taskRef: string | null = null;
|
|
53
69
|
let tui: string | null = null;
|
|
70
|
+
let recreate = false;
|
|
71
|
+
let separator = false;
|
|
54
72
|
for (let i = 0; i < args.length; i += 1) {
|
|
55
73
|
const arg = args[i] as string;
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
if (separator) {
|
|
75
|
+
forwarded.push(arg);
|
|
76
|
+
} else if (arg === '--') {
|
|
77
|
+
separator = true;
|
|
78
|
+
} else if (arg === '--skill') {
|
|
79
|
+
if (skill !== null) throw new Error("duplicate option '--skill'");
|
|
80
|
+
skill = optionValue(args, i, '--skill');
|
|
60
81
|
i += 1;
|
|
61
|
-
|
|
82
|
+
} else if (arg === '--task') {
|
|
83
|
+
if (taskRef !== null) throw new Error("duplicate option '--task'");
|
|
84
|
+
taskRef = optionValue(args, i, '--task');
|
|
85
|
+
i += 1;
|
|
86
|
+
} else if (arg === '--tui') {
|
|
87
|
+
if (tui !== null) throw new Error("duplicate option '--tui'");
|
|
88
|
+
tui = optionValue(args, i, '--tui');
|
|
89
|
+
i += 1;
|
|
90
|
+
} else if (arg === '--recreate') {
|
|
91
|
+
if (recreate) throw new Error("duplicate option '--recreate'");
|
|
92
|
+
recreate = true;
|
|
93
|
+
} else if (arg.startsWith('-')) {
|
|
94
|
+
throw new Error(`Unknown option '${arg}'`);
|
|
95
|
+
} else {
|
|
96
|
+
positionals.push(arg);
|
|
62
97
|
}
|
|
63
|
-
rest.push(arg);
|
|
64
98
|
}
|
|
65
|
-
return {
|
|
99
|
+
return { positionals, forwarded, skill, taskRef, tui, recreate };
|
|
66
100
|
}
|
|
67
101
|
|
|
68
102
|
export function parseRunArgs(args: string[]): ParsedRunArgs {
|
|
69
103
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
70
104
|
throw new Error(USAGE);
|
|
71
105
|
}
|
|
72
|
-
const {
|
|
73
|
-
|
|
74
|
-
const rest = withRecoveryFlags.filter((arg) => arg !== '--recreate');
|
|
75
|
-
const [skill = '', maybeTaskRef, ...remaining] = rest;
|
|
106
|
+
const { positionals, forwarded, skill, taskRef, tui, recreate } = extractHostOptions(args);
|
|
107
|
+
if (!skill) throw new Error("option '--skill' is required");
|
|
76
108
|
const spec = getSkillRunSpec(skill);
|
|
77
109
|
if (!spec) throw new Error(`Unknown skill '${skill}'`);
|
|
78
110
|
if (spec.kind === 'create') {
|
|
111
|
+
if (taskRef !== null) throw new Error("create-task does not accept '--task'");
|
|
79
112
|
if (recreate) throw new Error('--recreate is only valid for sandbox task runs');
|
|
80
|
-
const createArgs =
|
|
113
|
+
const createArgs = [...positionals, ...forwarded];
|
|
81
114
|
if (createArgs.length === 0) throw new Error('create-task requires a description');
|
|
82
115
|
return { skill, taskRef: null, args: createArgs, tui };
|
|
83
116
|
}
|
|
84
|
-
if (
|
|
117
|
+
if (positionals.length > 0) {
|
|
118
|
+
throw new Error('ai run requires --skill and --task; positional host arguments are not supported');
|
|
119
|
+
}
|
|
120
|
+
if (taskRef === null) throw new Error(`${skill} requires a task-ref`);
|
|
85
121
|
return {
|
|
86
122
|
skill,
|
|
87
|
-
taskRef
|
|
88
|
-
args:
|
|
123
|
+
taskRef,
|
|
124
|
+
args: forwarded,
|
|
89
125
|
tui,
|
|
90
126
|
...(recreate ? { recreate: true as const } : {})
|
|
91
127
|
};
|
|
@@ -200,8 +236,7 @@ export async function runSkill(args: string[], options: RunSkillOptions = {}): P
|
|
|
200
236
|
const commandConfig = options.command ?? config?.command ?? {};
|
|
201
237
|
assertAllowedByConfig(parsed.skill, commandConfig);
|
|
202
238
|
const tui = selectTui(parsed.skill, { cliTui: parsed.tui, command: commandConfig });
|
|
203
|
-
const
|
|
204
|
-
const prompt = renderPrompt({ tui, skill: parsed.skill, args: promptArgs });
|
|
239
|
+
const prompt = renderPrompt({ tui, skill: parsed.skill, args: parsed.args });
|
|
205
240
|
const [file, argv] = buildTuiCommand(tui, prompt);
|
|
206
241
|
const command = [file, ...argv];
|
|
207
242
|
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
} from './controller-registration.ts';
|
|
21
21
|
import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
22
22
|
import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
|
|
23
|
-
import { readSandboxControlStatus } from './state.ts';
|
|
23
|
+
import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts';
|
|
24
24
|
import type { TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
25
25
|
|
|
26
26
|
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
@@ -28,11 +28,13 @@ const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
|
28
28
|
export class SandboxControlClientError extends Error {
|
|
29
29
|
readonly detail: SandboxControlError;
|
|
30
30
|
readonly accepted: boolean;
|
|
31
|
-
|
|
31
|
+
readonly requestId: string | null;
|
|
32
|
+
constructor(detail: SandboxControlError, accepted = false, requestId: string | null = null) {
|
|
32
33
|
super(detail.message);
|
|
33
34
|
this.name = 'SandboxControlClientError';
|
|
34
35
|
this.detail = detail;
|
|
35
36
|
this.accepted = accepted;
|
|
37
|
+
this.requestId = requestId;
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -40,8 +42,14 @@ function sleep(milliseconds: number): void {
|
|
|
40
42
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
function clientError(
|
|
44
|
-
|
|
45
|
+
function clientError(
|
|
46
|
+
code: string,
|
|
47
|
+
message: string,
|
|
48
|
+
retryable: boolean,
|
|
49
|
+
accepted = false,
|
|
50
|
+
requestId: string | null = null
|
|
51
|
+
): never {
|
|
52
|
+
throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted, requestId);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
function preflight(statusDir: string, generation: string, now = Date.now()): void {
|
|
@@ -68,13 +76,35 @@ function preflight(statusDir: string, generation: string, now = Date.now()): voi
|
|
|
68
76
|
function parseResponse(raw: string, id: string): SandboxControlResponse {
|
|
69
77
|
const response = JSON.parse(raw) as SandboxControlResponse;
|
|
70
78
|
if (response.version !== 2 || response.id !== id
|
|
71
|
-
|| !['
|
|
72
|
-
|| (response.phase === 'completed' && !Number.isInteger(response.exitCode))
|
|
73
|
-
|
|
79
|
+
|| !['completed', 'rejected'].includes(response.phase)
|
|
80
|
+
|| (response.phase === 'completed' && !Number.isInteger(response.exitCode))
|
|
81
|
+
|| (response.outputState !== undefined && response.outputState !== 'available' && response.outputState !== 'unavailable')
|
|
82
|
+
|| (response.outputState === 'available' && !response.payload)
|
|
83
|
+
|| (response.outputState === 'unavailable' && response.payload !== null && response.payload !== undefined)) {
|
|
84
|
+
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response is invalid', false, false, id);
|
|
74
85
|
}
|
|
75
86
|
return response;
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
function readPublishedResponse(raw: string, id: string, channelDir: string): SandboxControlResponse {
|
|
90
|
+
const response = parseResponse(raw, id);
|
|
91
|
+
if (response.outputState !== 'available') return response;
|
|
92
|
+
const filePath = path.join(channelDir, 'responses', `${id}.payload.json`);
|
|
93
|
+
let payload;
|
|
94
|
+
try {
|
|
95
|
+
payload = readSandboxControlPayload(filePath);
|
|
96
|
+
} catch {
|
|
97
|
+
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker payload is missing or invalid', false, true, id);
|
|
98
|
+
}
|
|
99
|
+
if (!response.payload || response.payload.version !== payload.version
|
|
100
|
+
|| response.payload.id !== payload.id || response.payload.generation !== payload.generation
|
|
101
|
+
|| response.payload.stdoutBytes !== payload.stdoutBytes || response.payload.stderrBytes !== payload.stderrBytes
|
|
102
|
+
|| response.payload.stdoutSha256 !== payload.stdoutSha256 || response.payload.stderrSha256 !== payload.stderrSha256) {
|
|
103
|
+
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker payload reference is invalid', false, true, id);
|
|
104
|
+
}
|
|
105
|
+
return { ...response, stdout: payload.stdout, stderr: payload.stderr };
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
function cancelPendingRequest(requestPath: string): boolean {
|
|
79
109
|
try {
|
|
80
110
|
fs.unlinkSync(requestPath);
|
|
@@ -95,13 +125,16 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
95
125
|
preflight(statusDir, request.generation);
|
|
96
126
|
const encoded = `${JSON.stringify(request)}\n`;
|
|
97
127
|
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
98
|
-
clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false);
|
|
128
|
+
clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false, false, request.id);
|
|
99
129
|
}
|
|
100
130
|
const requestsDir = path.join(channelDir, 'requests');
|
|
101
131
|
const responsesDir = path.join(channelDir, 'responses');
|
|
102
132
|
const temporary = path.join(requestsDir, `.${request.id}.tmp`);
|
|
103
133
|
const requestPath = path.join(requestsDir, `${request.id}.json`);
|
|
104
134
|
const responsePath = path.join(responsesDir, `${request.id}.json`);
|
|
135
|
+
if (fs.existsSync(responsePath)) {
|
|
136
|
+
clientError('SANDBOX_CONTROL_REQUEST_REPLAYED', 'request id already has a terminal response', false, false, request.id);
|
|
137
|
+
}
|
|
105
138
|
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
106
139
|
fs.renameSync(temporary, requestPath);
|
|
107
140
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
@@ -109,6 +142,8 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
109
142
|
let malformedResponseRaw: string | null = null;
|
|
110
143
|
let malformedResponseObservedAt = 0;
|
|
111
144
|
while (Date.now() < deadline) {
|
|
145
|
+
const acceptedPath = path.join(responsesDir, `${request.id}.accepted.json`);
|
|
146
|
+
if (fs.existsSync(acceptedPath)) accepted = true;
|
|
112
147
|
if (fs.existsSync(responsePath)) {
|
|
113
148
|
let raw: string;
|
|
114
149
|
try {
|
|
@@ -119,7 +154,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
119
154
|
}
|
|
120
155
|
let response: SandboxControlResponse;
|
|
121
156
|
try {
|
|
122
|
-
response =
|
|
157
|
+
response = readPublishedResponse(raw, request.id, channelDir);
|
|
123
158
|
malformedResponseRaw = null;
|
|
124
159
|
} catch (error) {
|
|
125
160
|
if (!(error instanceof SyntaxError)) throw error;
|
|
@@ -128,7 +163,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
128
163
|
malformedResponseRaw = raw;
|
|
129
164
|
malformedResponseObservedAt = now;
|
|
130
165
|
} else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
|
|
131
|
-
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted);
|
|
166
|
+
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted, request.id);
|
|
132
167
|
}
|
|
133
168
|
sleep(25);
|
|
134
169
|
continue;
|
|
@@ -136,7 +171,6 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
136
171
|
if (response.phase === 'accepted') {
|
|
137
172
|
accepted = true;
|
|
138
173
|
} else {
|
|
139
|
-
fs.rmSync(responsePath, { force: true });
|
|
140
174
|
return response;
|
|
141
175
|
}
|
|
142
176
|
}
|
|
@@ -149,7 +183,8 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
149
183
|
'SANDBOX_CONTROL_RESULT_UNKNOWN',
|
|
150
184
|
'request was claimed before broker availability was lost; inspect domain state before retrying',
|
|
151
185
|
false,
|
|
152
|
-
true
|
|
186
|
+
true,
|
|
187
|
+
request.id
|
|
153
188
|
);
|
|
154
189
|
}
|
|
155
190
|
}
|
|
@@ -160,7 +195,8 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
160
195
|
'SANDBOX_CONTROL_RESULT_UNKNOWN',
|
|
161
196
|
'accepted request did not produce a final result; inspect domain state before retrying',
|
|
162
197
|
false,
|
|
163
|
-
true
|
|
198
|
+
true,
|
|
199
|
+
request.id
|
|
164
200
|
);
|
|
165
201
|
}
|
|
166
202
|
if (!cancelPendingRequest(requestPath)) {
|
|
@@ -168,10 +204,64 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
168
204
|
'SANDBOX_CONTROL_RESULT_UNKNOWN',
|
|
169
205
|
'request was claimed but no acceptance result was observed; inspect domain state before retrying',
|
|
170
206
|
false,
|
|
171
|
-
true
|
|
207
|
+
true,
|
|
208
|
+
request.id
|
|
172
209
|
);
|
|
173
210
|
}
|
|
174
|
-
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', `broker did not accept the request within ${params.timeoutMs ?? 30_000}ms`, true);
|
|
211
|
+
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', `broker did not accept the request within ${params.timeoutMs ?? 30_000}ms`, true, false, request.id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function recoverSandboxControl(requestId: string, params: Readonly<{
|
|
215
|
+
channelDir?: string;
|
|
216
|
+
timeoutMs?: number;
|
|
217
|
+
}> = {}): SandboxControlResponse {
|
|
218
|
+
if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
|
|
219
|
+
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
|
|
220
|
+
}
|
|
221
|
+
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
222
|
+
const responsePath = path.join(channelDir, 'responses', `${requestId}.json`);
|
|
223
|
+
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
224
|
+
let accepted = false;
|
|
225
|
+
let malformedResponseRaw: string | null = null;
|
|
226
|
+
let malformedResponseObservedAt = 0;
|
|
227
|
+
while (Date.now() < deadline) {
|
|
228
|
+
if (fs.existsSync(path.join(channelDir, 'responses', `${requestId}.accepted.json`))) accepted = true;
|
|
229
|
+
if (fs.existsSync(responsePath)) {
|
|
230
|
+
let raw: string;
|
|
231
|
+
try {
|
|
232
|
+
raw = fs.readFileSync(responsePath, 'utf8');
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue;
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
let response: SandboxControlResponse;
|
|
238
|
+
try {
|
|
239
|
+
response = readPublishedResponse(raw, requestId, channelDir);
|
|
240
|
+
malformedResponseRaw = null;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
if (malformedResponseRaw !== raw) {
|
|
245
|
+
malformedResponseRaw = raw;
|
|
246
|
+
malformedResponseObservedAt = now;
|
|
247
|
+
} else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
|
|
248
|
+
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted, requestId);
|
|
249
|
+
}
|
|
250
|
+
sleep(25);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (response.phase === 'accepted') accepted = true;
|
|
254
|
+
else return response;
|
|
255
|
+
}
|
|
256
|
+
sleep(25);
|
|
257
|
+
}
|
|
258
|
+
clientError(
|
|
259
|
+
'SANDBOX_CONTROL_RESULT_UNKNOWN',
|
|
260
|
+
'request did not produce a final result; inspect domain state before retrying',
|
|
261
|
+
false,
|
|
262
|
+
accepted,
|
|
263
|
+
requestId
|
|
264
|
+
);
|
|
175
265
|
}
|
|
176
266
|
|
|
177
267
|
function authority(params: { token?: string; generation?: string }): { token: string; generation: string } {
|
|
@@ -218,7 +218,7 @@ function lifecycleFailure(code: string, message: string): SandboxControlExecutio
|
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function finalizationResult(result: ReturnType<typeof applyTaskFinalization
|
|
221
|
+
function finalizationResult(result: Awaited<ReturnType<typeof applyTaskFinalization>>): SandboxControlExecutionResult {
|
|
222
222
|
const payload = {
|
|
223
223
|
version: 1,
|
|
224
224
|
status: result.status,
|
|
@@ -246,7 +246,7 @@ export async function executeRequest(
|
|
|
246
246
|
options: ExecuteRequestOptions = {}
|
|
247
247
|
): Promise<SandboxControlExecutionResult> {
|
|
248
248
|
if (request.family === 'task-create') {
|
|
249
|
-
const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
249
|
+
const result = await createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
250
250
|
return {
|
|
251
251
|
exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
|
|
252
252
|
stdout: `${JSON.stringify(result)}\n`,
|
|
@@ -313,7 +313,7 @@ export async function executeRequest(
|
|
|
313
313
|
}),
|
|
314
314
|
operation
|
|
315
315
|
);
|
|
316
|
-
return finalizationResult(result);
|
|
316
|
+
return finalizationResult(await result);
|
|
317
317
|
}
|
|
318
318
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
|
|
319
319
|
if (request.family === 'task-orchestration') {
|
|
@@ -387,7 +387,7 @@ export async function executeRequest(
|
|
|
387
387
|
return format(await dispatchTaskControlOperation(context, operation));
|
|
388
388
|
}
|
|
389
389
|
if (operation.family !== 'task-lifecycle') throw new Error('SANDBOX_CONTROL_LIFECYCLE_OPERATION_INVALID');
|
|
390
|
-
|
|
390
|
+
return format(await dispatchTaskControlOperation(context, operation));
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
export async function runSandboxControlExecutor(requestPath: string, nonce: string): Promise<void> {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
2
3
|
import { normalizeAgentToken } from '../../agent-clients/tokens.ts';
|
|
3
4
|
import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
4
5
|
import type { CodexControllerLeaseProofV1 } from './controller-registration.ts';
|
|
5
6
|
|
|
6
7
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
8
|
+
export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
|
|
9
|
+
export const SANDBOX_CONTROL_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
export const SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES = 1024 * 1024;
|
|
11
|
+
export const SANDBOX_CONTROL_RESERVATION_BYTES = SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES + 4 * 1024;
|
|
7
12
|
export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
8
13
|
export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
9
14
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
@@ -63,9 +68,43 @@ export type SandboxCodexControllerRequest = RequestBase & Readonly<{
|
|
|
63
68
|
}>;
|
|
64
69
|
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskFinalizationRequest | SandboxTaskCreateRequest | SandboxCodexControllerRequest;
|
|
65
70
|
export type SandboxControlError = Readonly<{ code: string; message: string; retryable: boolean }>;
|
|
71
|
+
export type SandboxControlResultEvidence = Readonly<{
|
|
72
|
+
version: 1;
|
|
73
|
+
id: string;
|
|
74
|
+
generation: string;
|
|
75
|
+
exitCode: number;
|
|
76
|
+
stdoutBytes: number;
|
|
77
|
+
stderrBytes: number;
|
|
78
|
+
stdoutSha256: string;
|
|
79
|
+
stderrSha256: string;
|
|
80
|
+
captureState: 'metadata-only';
|
|
81
|
+
}>;
|
|
82
|
+
export type SandboxControlPayloadReference = Readonly<{
|
|
83
|
+
version: 1;
|
|
84
|
+
id: string;
|
|
85
|
+
generation: string;
|
|
86
|
+
stdoutBytes: number;
|
|
87
|
+
stderrBytes: number;
|
|
88
|
+
stdoutSha256: string;
|
|
89
|
+
stderrSha256: string;
|
|
90
|
+
}>;
|
|
91
|
+
export type SandboxControlPayload = SandboxControlPayloadReference & Readonly<{
|
|
92
|
+
stdout: string;
|
|
93
|
+
stderr: string;
|
|
94
|
+
}>;
|
|
95
|
+
export type SandboxControlReservation = Readonly<{
|
|
96
|
+
version: 1;
|
|
97
|
+
id: string;
|
|
98
|
+
generation: string;
|
|
99
|
+
logicalRecords: 1;
|
|
100
|
+
bytes: number;
|
|
101
|
+
createdAt: number;
|
|
102
|
+
}>;
|
|
66
103
|
export type SandboxControlResponse = Readonly<{
|
|
67
104
|
version: 2; id: string; phase: 'accepted' | 'completed' | 'rejected'; exitCode: number | null;
|
|
68
105
|
stdout: string; stderr: string; error: SandboxControlError | null;
|
|
106
|
+
outputState?: 'available' | 'unavailable';
|
|
107
|
+
payload?: SandboxControlPayloadReference | null;
|
|
69
108
|
}>;
|
|
70
109
|
export type SandboxControlStatus = Readonly<{
|
|
71
110
|
version: 2; generation: string; broker: ProcessIdentity & { brokerId: string };
|
|
@@ -97,6 +136,80 @@ function fail(code: string, message: string, retryable = false): never {
|
|
|
97
136
|
throw new SandboxControlProtocolError(code, message, retryable);
|
|
98
137
|
}
|
|
99
138
|
|
|
139
|
+
export function parseSandboxControlResultEvidence(value: unknown): SandboxControlResultEvidence {
|
|
140
|
+
const evidence = value as Partial<SandboxControlResultEvidence> | null;
|
|
141
|
+
const keys = evidence && typeof evidence === 'object' ? Object.keys(evidence).sort().join(',') : '';
|
|
142
|
+
if (keys !== 'captureState,exitCode,generation,id,stderrBytes,stderrSha256,stdoutBytes,stdoutSha256,version'
|
|
143
|
+
|| evidence?.version !== 1
|
|
144
|
+
|| typeof evidence.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(evidence.id)
|
|
145
|
+
|| typeof evidence.generation !== 'string' || evidence.generation.length === 0
|
|
146
|
+
|| !Number.isSafeInteger(evidence.exitCode)
|
|
147
|
+
|| !Number.isSafeInteger(evidence.stdoutBytes) || (evidence.stdoutBytes as number) < 0
|
|
148
|
+
|| !Number.isSafeInteger(evidence.stderrBytes) || (evidence.stderrBytes as number) < 0
|
|
149
|
+
|| (evidence.stdoutBytes as number) + (evidence.stderrBytes as number) > SANDBOX_CONTROL_MAX_RESPONSE_BYTES
|
|
150
|
+
|| typeof evidence.stdoutSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(evidence.stdoutSha256)
|
|
151
|
+
|| typeof evidence.stderrSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(evidence.stderrSha256)
|
|
152
|
+
|| evidence.captureState !== 'metadata-only') {
|
|
153
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
154
|
+
}
|
|
155
|
+
return evidence as SandboxControlResultEvidence;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validPayloadReference(value: unknown, id: string, generation: string): value is SandboxControlPayloadReference {
|
|
159
|
+
const payload = value as Partial<SandboxControlPayloadReference> | null;
|
|
160
|
+
const keys = payload && typeof payload === 'object' ? Object.keys(payload).sort().join(',') : '';
|
|
161
|
+
return keys === 'generation,id,stderrBytes,stderrSha256,stdoutBytes,stdoutSha256,version'
|
|
162
|
+
&& payload?.version === 1
|
|
163
|
+
&& payload.id === id
|
|
164
|
+
&& payload.generation === generation
|
|
165
|
+
&& Number.isSafeInteger(payload.stdoutBytes) && (payload.stdoutBytes as number) >= 0
|
|
166
|
+
&& Number.isSafeInteger(payload.stderrBytes) && (payload.stderrBytes as number) >= 0
|
|
167
|
+
&& (payload.stdoutBytes as number) + (payload.stderrBytes as number) <= SANDBOX_CONTROL_MAX_RESPONSE_BYTES
|
|
168
|
+
&& typeof payload.stdoutSha256 === 'string' && /^[a-f0-9]{64}$/u.test(payload.stdoutSha256)
|
|
169
|
+
&& typeof payload.stderrSha256 === 'string' && /^[a-f0-9]{64}$/u.test(payload.stderrSha256);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function parseSandboxControlPayload(value: unknown): SandboxControlPayload {
|
|
173
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('SANDBOX_CONTROL_PAYLOAD_INVALID');
|
|
174
|
+
const payload = value as Partial<SandboxControlPayload>;
|
|
175
|
+
const reference: Partial<SandboxControlPayloadReference> = {
|
|
176
|
+
version: payload.version,
|
|
177
|
+
id: payload.id,
|
|
178
|
+
generation: payload.generation,
|
|
179
|
+
stdoutBytes: payload.stdoutBytes,
|
|
180
|
+
stderrBytes: payload.stderrBytes,
|
|
181
|
+
stdoutSha256: payload.stdoutSha256,
|
|
182
|
+
stderrSha256: payload.stderrSha256
|
|
183
|
+
};
|
|
184
|
+
const keys = Object.keys(payload).sort().join(',');
|
|
185
|
+
if (keys !== 'generation,id,stderr,stderrBytes,stderrSha256,stdout,stdoutBytes,stdoutSha256,version'
|
|
186
|
+
|| !validPayloadReference(reference, reference.id as string, reference.generation as string)
|
|
187
|
+
|| typeof payload.stdout !== 'string' || typeof payload.stderr !== 'string'
|
|
188
|
+
|| Buffer.byteLength(payload.stdout, 'utf8') !== payload.stdoutBytes
|
|
189
|
+
|| Buffer.byteLength(payload.stderr, 'utf8') !== payload.stderrBytes
|
|
190
|
+
|| createHash('sha256').update(payload.stdout, 'utf8').digest('hex') !== payload.stdoutSha256
|
|
191
|
+
|| createHash('sha256').update(payload.stderr, 'utf8').digest('hex') !== payload.stderrSha256) {
|
|
192
|
+
throw new Error('SANDBOX_CONTROL_PAYLOAD_INVALID');
|
|
193
|
+
}
|
|
194
|
+
return payload as SandboxControlPayload;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function parseSandboxControlReservation(value: unknown): SandboxControlReservation {
|
|
198
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('SANDBOX_CONTROL_RESERVATION_INVALID');
|
|
199
|
+
const reservation = value as Partial<SandboxControlReservation>;
|
|
200
|
+
const keys = Object.keys(reservation).sort().join(',');
|
|
201
|
+
if (keys !== 'bytes,createdAt,generation,id,logicalRecords,version'
|
|
202
|
+
|| reservation.version !== 1
|
|
203
|
+
|| typeof reservation.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(reservation.id)
|
|
204
|
+
|| typeof reservation.generation !== 'string' || reservation.generation.length === 0
|
|
205
|
+
|| reservation.logicalRecords !== 1
|
|
206
|
+
|| !Number.isSafeInteger(reservation.bytes) || (reservation.bytes as number) < SANDBOX_CONTROL_RESERVATION_BYTES
|
|
207
|
+
|| !Number.isSafeInteger(reservation.createdAt)) {
|
|
208
|
+
throw new Error('SANDBOX_CONTROL_RESERVATION_INVALID');
|
|
209
|
+
}
|
|
210
|
+
return reservation as SandboxControlReservation;
|
|
211
|
+
}
|
|
212
|
+
|
|
100
213
|
export function isSandboxControlFamily(value: string): value is SandboxControlFamily {
|
|
101
214
|
return SANDBOX_CONTROL_FAMILIES.includes(value as SandboxControlFamily);
|
|
102
215
|
}
|