@zq-silk/yui 0.2.0 → 0.4.2
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/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
5
|
+
export const YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR = "YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR";
|
|
6
|
+
export const YUI_TASK_RUNTIME_SERVICE_NAMESPACE = "YUI_TASK_RUNTIME_SERVICE_NAMESPACE";
|
|
7
|
+
const MARKER_FILE = ".yui-task-runtime-owner.json";
|
|
8
|
+
/**
|
|
9
|
+
* One project-neutral implementation owns descriptor construction, preflight,
|
|
10
|
+
* exact generation activation, and exact generation cleanup. It never scans by
|
|
11
|
+
* process name, Role, PID, age, or an ambient Home.
|
|
12
|
+
*/
|
|
13
|
+
export class FileTaskRuntimeIsolation {
|
|
14
|
+
#runtimeRoot;
|
|
15
|
+
#controlPlane;
|
|
16
|
+
#pathLayout;
|
|
17
|
+
#inspectResources;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.#runtimeRoot = canonicalPath(options.runtimeRoot, "Task runtime root");
|
|
20
|
+
this.#controlPlane = normalizeControlBoundary(options.controlPlane);
|
|
21
|
+
this.#pathLayout = taskRuntimePathLayout(options.pathLayout);
|
|
22
|
+
this.#inspectResources = options.inspectResources;
|
|
23
|
+
}
|
|
24
|
+
preflight(input) {
|
|
25
|
+
const descriptor = createTaskRuntimeIsolationDescriptor({
|
|
26
|
+
...input,
|
|
27
|
+
runtimeRoot: this.#runtimeRoot,
|
|
28
|
+
pathLayout: this.#pathLayout
|
|
29
|
+
});
|
|
30
|
+
const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
|
|
31
|
+
assertTaskRuntimeIsolationPreflight({
|
|
32
|
+
descriptor,
|
|
33
|
+
workspace: input.workspace,
|
|
34
|
+
runtimeRoot: this.#runtimeRoot,
|
|
35
|
+
pathLayout: this.#pathLayout,
|
|
36
|
+
controlPlane: this.#controlPlane,
|
|
37
|
+
resources: [
|
|
38
|
+
...inspectGenerationRoot(descriptor, fingerprint),
|
|
39
|
+
...(this.#inspectResources?.(descriptor) ?? [])
|
|
40
|
+
],
|
|
41
|
+
allowExactActive: input.allowExactActive === true
|
|
42
|
+
});
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
descriptor,
|
|
45
|
+
fingerprint,
|
|
46
|
+
environment: taskRuntimeIsolationEnvironment(descriptor)
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
activate(preparation) {
|
|
50
|
+
const { descriptor, fingerprint } = validatePreparation(preparation);
|
|
51
|
+
const root = descriptor.roots.generation;
|
|
52
|
+
const existing = inspectGenerationRoot(descriptor, fingerprint);
|
|
53
|
+
if (existing.length > 0) {
|
|
54
|
+
const [resource] = existing;
|
|
55
|
+
if (resource?.ownership !== "owned"
|
|
56
|
+
|| resource.descriptorFingerprint !== fingerprint) {
|
|
57
|
+
throw new Error(`Task runtime generation is not exactly owned: ${root}.`);
|
|
58
|
+
}
|
|
59
|
+
ensureOwnedDirectories(descriptor);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
ensureDirectoryChain(this.#runtimeRoot, dirname(root));
|
|
63
|
+
mkdirSync(root, { mode: 0o700 });
|
|
64
|
+
try {
|
|
65
|
+
const marker = {
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
kind: "yui-task-runtime-resource-owner",
|
|
68
|
+
fingerprint,
|
|
69
|
+
descriptor
|
|
70
|
+
};
|
|
71
|
+
writeFileSync(join(root, MARKER_FILE), `${JSON.stringify(marker)}\n`, {
|
|
72
|
+
flag: "wx",
|
|
73
|
+
mode: 0o600
|
|
74
|
+
});
|
|
75
|
+
ensureOwnedDirectories(descriptor);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const current = inspectGenerationRoot(descriptor, fingerprint);
|
|
79
|
+
if (current[0]?.ownership === "owned")
|
|
80
|
+
throw error;
|
|
81
|
+
// The directory was created by this exact activation but never acquired
|
|
82
|
+
// its marker. Remove it only while it remains empty; concurrent unmarked
|
|
83
|
+
// content is ambiguous and must be preserved.
|
|
84
|
+
rmdirSync(root);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
cleanup(preparation, reason) {
|
|
89
|
+
requireCleanupReason(reason);
|
|
90
|
+
const { descriptor, fingerprint } = validatePreparation(preparation);
|
|
91
|
+
const resources = [
|
|
92
|
+
...inspectGenerationRoot(descriptor, fingerprint),
|
|
93
|
+
...(this.#inspectResources?.(descriptor) ?? [])
|
|
94
|
+
];
|
|
95
|
+
if (resources.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
planTaskRuntimeCleanup(descriptor, reason, resources);
|
|
98
|
+
// Re-read the sole durable marker immediately before deletion. A missing,
|
|
99
|
+
// replaced, symlinked, or mismatched generation is never cleanup authority.
|
|
100
|
+
const current = inspectGenerationRoot(descriptor, fingerprint);
|
|
101
|
+
if (current.length !== 1
|
|
102
|
+
|| current[0]?.ownership !== "owned"
|
|
103
|
+
|| current[0].descriptorFingerprint !== fingerprint) {
|
|
104
|
+
throw new Error("Task runtime resources changed since cleanup preflight.");
|
|
105
|
+
}
|
|
106
|
+
const root = descriptor.roots.generation;
|
|
107
|
+
const claimed = `${root}.cleanup-${randomBytes(16).toString("hex")}`;
|
|
108
|
+
renameSync(root, claimed);
|
|
109
|
+
try {
|
|
110
|
+
const marker = parseMarker(readFileSync(join(claimed, MARKER_FILE), "utf8"));
|
|
111
|
+
if (marker.fingerprint !== fingerprint
|
|
112
|
+
|| JSON.stringify(marker.descriptor) !== JSON.stringify(descriptor)) {
|
|
113
|
+
throw new Error("Task runtime resources changed during cleanup claim.");
|
|
114
|
+
}
|
|
115
|
+
rmSync(claimed, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// Preserve a claimed-but-unverified resource. Restore its exact path only
|
|
119
|
+
// when no concurrent generation has appeared there; never delete it.
|
|
120
|
+
try {
|
|
121
|
+
renameSync(claimed, root);
|
|
122
|
+
}
|
|
123
|
+
catch (restoreError) {
|
|
124
|
+
throw new Error("Task runtime cleanup claim could not be verified or restored.", { cause: restoreError });
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
cleanupTaskLaunch(input) {
|
|
130
|
+
const taskId = requireIdentity(input.taskId, "Task id");
|
|
131
|
+
const launchId = requireIdentity(input.launchId, "Launch id");
|
|
132
|
+
requireCleanupReason(input.reason);
|
|
133
|
+
const taskRoot = taskRuntimeInventoryRoot(this.#runtimeRoot, taskId, this.#pathLayout);
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
const metadata = lstatSync(taskRoot);
|
|
137
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
138
|
+
throw new Error("Task runtime Task root is ambiguous.");
|
|
139
|
+
}
|
|
140
|
+
entries = readdirSync(taskRoot, { withFileTypes: true });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (isNodeCode(error, "ENOENT"))
|
|
144
|
+
return "absent";
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
const launchDigest = taskRuntimeLaunchDigest(taskId, launchId, this.#pathLayout);
|
|
148
|
+
const matches = [];
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
151
|
+
throw new Error("Task runtime owner inventory is ambiguous.");
|
|
152
|
+
}
|
|
153
|
+
const generationRoot = join(taskRoot, entry.name, launchDigest);
|
|
154
|
+
let metadata;
|
|
155
|
+
try {
|
|
156
|
+
metadata = lstatSync(generationRoot);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (isNodeCode(error, "ENOENT"))
|
|
160
|
+
continue;
|
|
161
|
+
throw new Error("Task runtime generation inventory is ambiguous.", { cause: error });
|
|
162
|
+
}
|
|
163
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
164
|
+
throw new Error("Task runtime generation inventory is ambiguous.");
|
|
165
|
+
}
|
|
166
|
+
let marker;
|
|
167
|
+
try {
|
|
168
|
+
marker = parseMarker(readFileSync(join(generationRoot, MARKER_FILE), "utf8"));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
throw new Error("Task runtime launch generation is unmarked or invalid.", {
|
|
172
|
+
cause: error
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const descriptor = marker.descriptor;
|
|
176
|
+
const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
|
|
177
|
+
if (marker.fingerprint !== fingerprint
|
|
178
|
+
|| descriptor.taskId !== taskId
|
|
179
|
+
|| descriptor.generation.launchId !== launchId
|
|
180
|
+
|| descriptor.roots.generation !== generationRoot) {
|
|
181
|
+
throw new Error("Task runtime launch generation ownership is mismatched.");
|
|
182
|
+
}
|
|
183
|
+
matches.push({
|
|
184
|
+
descriptor,
|
|
185
|
+
fingerprint,
|
|
186
|
+
environment: taskRuntimeIsolationEnvironment(descriptor)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (matches.length === 0)
|
|
190
|
+
return "absent";
|
|
191
|
+
if (matches.length !== 1) {
|
|
192
|
+
throw new Error("Task runtime launch generation ownership is ambiguous.");
|
|
193
|
+
}
|
|
194
|
+
this.cleanup(matches[0], input.reason);
|
|
195
|
+
return "cleaned";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export function createTaskRuntimeIsolationDescriptor(input) {
|
|
199
|
+
const workspace = validateManagedWorkspace(input.workspace);
|
|
200
|
+
const owner = taskRuntimeWorkspaceOwner(workspace.owner);
|
|
201
|
+
const taskId = requireIdentity(owner.taskId, "Task id");
|
|
202
|
+
const runId = input.runId === undefined
|
|
203
|
+
? undefined
|
|
204
|
+
: requireIdentity(input.runId, "Run id");
|
|
205
|
+
const launchId = requireIdentity(input.launchId, "Launch id");
|
|
206
|
+
const generationId = requireIdentity(input.generationId, "Generation id");
|
|
207
|
+
const runtimeRoot = canonicalPath(input.runtimeRoot, "Task runtime root");
|
|
208
|
+
const pathLayout = taskRuntimePathLayout(input.pathLayout);
|
|
209
|
+
const generation = taskRuntimeGenerationRoot(runtimeRoot, taskId, owner, launchId, pathLayout);
|
|
210
|
+
const declared = capabilities(input.policy?.declaredExternalCapabilities ?? [], "Declared external capability");
|
|
211
|
+
const requested = capabilities(input.policy?.requestedExternalCapabilities ?? [], "Requested external capability");
|
|
212
|
+
const portPreference = ports(input.policy?.portPreference ?? [], "Port preference");
|
|
213
|
+
const portAllocations = allocations(input.policy?.portAllocations ?? []);
|
|
214
|
+
return Object.freeze({
|
|
215
|
+
schemaVersion: 1,
|
|
216
|
+
kind: "yui-task-runtime-isolation",
|
|
217
|
+
taskId,
|
|
218
|
+
workspace: Object.freeze({ owner, root: canonicalPath(workspace.root, "Workspace root") }),
|
|
219
|
+
roots: Object.freeze({
|
|
220
|
+
generation,
|
|
221
|
+
data: join(generation, "data"),
|
|
222
|
+
cache: join(generation, "cache"),
|
|
223
|
+
temporary: join(generation, "tmp")
|
|
224
|
+
}),
|
|
225
|
+
serviceNamespace: taskRuntimeServiceNamespace(taskId, owner, launchId, generationId),
|
|
226
|
+
portPreference,
|
|
227
|
+
portAllocations,
|
|
228
|
+
externalCapabilities: Object.freeze({ declared, requested }),
|
|
229
|
+
generation: Object.freeze({
|
|
230
|
+
...(runId === undefined ? {} : { runId }),
|
|
231
|
+
launchId,
|
|
232
|
+
generationId
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
export function parseTaskRuntimeIsolationDescriptor(serialized) {
|
|
237
|
+
let value;
|
|
238
|
+
try {
|
|
239
|
+
value = JSON.parse(serialized);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
throw new Error("Task runtime isolation descriptor is invalid JSON.");
|
|
243
|
+
}
|
|
244
|
+
if (!isRecord(value) || value.kind !== "yui-task-runtime-isolation") {
|
|
245
|
+
throw new Error(`Expected yui-task-runtime-isolation descriptor; found ${isRecord(value) && typeof value.kind === "string" ? value.kind : "unknown"}.`);
|
|
246
|
+
}
|
|
247
|
+
if (value.schemaVersion !== 1) {
|
|
248
|
+
throw new Error("Task runtime isolation descriptor schema version is invalid.");
|
|
249
|
+
}
|
|
250
|
+
const workspace = requireRecord(value.workspace, "Task runtime workspace");
|
|
251
|
+
const roots = requireRecord(value.roots, "Task runtime roots");
|
|
252
|
+
const generation = requireRecord(value.generation, "Task runtime generation");
|
|
253
|
+
const external = requireRecord(value.externalCapabilities, "Task runtime external capabilities");
|
|
254
|
+
const owner = taskRuntimeWorkspaceOwner(requireRecord(workspace.owner, "Task runtime workspace owner"));
|
|
255
|
+
const descriptor = {
|
|
256
|
+
schemaVersion: 1,
|
|
257
|
+
kind: "yui-task-runtime-isolation",
|
|
258
|
+
taskId: requireIdentity(value.taskId, "Task id"),
|
|
259
|
+
workspace: {
|
|
260
|
+
owner,
|
|
261
|
+
root: canonicalPath(workspace.root, "Workspace root")
|
|
262
|
+
},
|
|
263
|
+
roots: {
|
|
264
|
+
generation: canonicalPath(roots.generation, "Task runtime generation root"),
|
|
265
|
+
data: canonicalPath(roots.data, "Task runtime data root"),
|
|
266
|
+
cache: canonicalPath(roots.cache, "Task runtime cache root"),
|
|
267
|
+
temporary: canonicalPath(roots.temporary, "Task runtime temporary root")
|
|
268
|
+
},
|
|
269
|
+
serviceNamespace: requireIdentity(value.serviceNamespace, "Service namespace"),
|
|
270
|
+
portPreference: ports(value.portPreference, "Port preference"),
|
|
271
|
+
portAllocations: allocations(value.portAllocations),
|
|
272
|
+
externalCapabilities: {
|
|
273
|
+
declared: capabilities(external.declared, "Declared external capability"),
|
|
274
|
+
requested: capabilities(external.requested, "Requested external capability")
|
|
275
|
+
},
|
|
276
|
+
generation: {
|
|
277
|
+
...(generation.runId === undefined
|
|
278
|
+
? {}
|
|
279
|
+
: { runId: requireIdentity(generation.runId, "Run id") }),
|
|
280
|
+
launchId: requireIdentity(generation.launchId, "Launch id"),
|
|
281
|
+
generationId: requireIdentity(generation.generationId, "Generation id")
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
return Object.freeze(descriptor);
|
|
285
|
+
}
|
|
286
|
+
export function taskRuntimeIsolationFingerprint(descriptor) {
|
|
287
|
+
return digest(JSON.stringify(parseTaskRuntimeIsolationDescriptor(JSON.stringify(descriptor))));
|
|
288
|
+
}
|
|
289
|
+
export function taskRuntimeIsolationEnvironment(descriptor) {
|
|
290
|
+
const validated = parseTaskRuntimeIsolationDescriptor(JSON.stringify(descriptor));
|
|
291
|
+
return Object.freeze({
|
|
292
|
+
TMPDIR: validated.roots.temporary,
|
|
293
|
+
XDG_CACHE_HOME: validated.roots.cache,
|
|
294
|
+
XDG_DATA_HOME: validated.roots.data,
|
|
295
|
+
XDG_STATE_HOME: join(validated.roots.data, "state"),
|
|
296
|
+
XDG_RUNTIME_DIR: join(validated.roots.temporary, "runtime"),
|
|
297
|
+
[YUI_TASK_RUNTIME_SERVICE_NAMESPACE]: validated.serviceNamespace,
|
|
298
|
+
[YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR]: JSON.stringify(validated)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
export function assertTaskRuntimeIsolationPreflight(input) {
|
|
302
|
+
const descriptor = parseTaskRuntimeIsolationDescriptor(JSON.stringify(input.descriptor));
|
|
303
|
+
const workspace = validateManagedWorkspace(input.workspace);
|
|
304
|
+
const expectedOwner = taskRuntimeWorkspaceOwner(workspace.owner);
|
|
305
|
+
if (descriptor.taskId !== expectedOwner.taskId
|
|
306
|
+
|| JSON.stringify(descriptor.workspace.owner) !== JSON.stringify(expectedOwner)
|
|
307
|
+
|| descriptor.workspace.root !== workspace.root) {
|
|
308
|
+
throw new Error("Task runtime owner or workspace does not match its ManagedWorkspace.");
|
|
309
|
+
}
|
|
310
|
+
const runtimeRoot = canonicalPath(input.runtimeRoot, "Task runtime root");
|
|
311
|
+
const pathLayout = taskRuntimePathLayout(input.pathLayout);
|
|
312
|
+
const expectedGeneration = taskRuntimeGenerationRoot(runtimeRoot, descriptor.taskId, expectedOwner, descriptor.generation.launchId, pathLayout);
|
|
313
|
+
if (descriptor.roots.generation !== expectedGeneration
|
|
314
|
+
|| descriptor.roots.data !== join(expectedGeneration, "data")
|
|
315
|
+
|| descriptor.roots.cache !== join(expectedGeneration, "cache")
|
|
316
|
+
|| descriptor.roots.temporary !== join(expectedGeneration, "tmp")) {
|
|
317
|
+
throw new Error("Task runtime roots do not match the exact Task, owner, and launch identity.");
|
|
318
|
+
}
|
|
319
|
+
if (descriptor.serviceNamespace !== taskRuntimeServiceNamespace(descriptor.taskId, expectedOwner, descriptor.generation.launchId, descriptor.generation.generationId)) {
|
|
320
|
+
throw new Error("Task runtime service namespace does not match its exact Task launch generation.");
|
|
321
|
+
}
|
|
322
|
+
if (!isWithin(runtimeRoot, descriptor.roots.generation)) {
|
|
323
|
+
throw new Error("Task runtime generation is outside its runtime root.");
|
|
324
|
+
}
|
|
325
|
+
for (const [name, root] of Object.entries(descriptor.roots)) {
|
|
326
|
+
if (name !== "generation" && !isWithin(descriptor.roots.generation, root)) {
|
|
327
|
+
throw new Error(`Task runtime ${name} root is outside its exact generation.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const control = normalizeControlBoundary(input.controlPlane);
|
|
331
|
+
const protectedPaths = [control.yuiHome, ...control.globalInstallPaths];
|
|
332
|
+
for (const root of Object.values(descriptor.roots)) {
|
|
333
|
+
if (protectedPaths.some((path) => pathsOverlap(root, path))) {
|
|
334
|
+
throw new Error("Task runtime roots overlap the control YUI_HOME or a global install path.");
|
|
335
|
+
}
|
|
336
|
+
if (pathsOverlap(root, descriptor.workspace.root)) {
|
|
337
|
+
throw new Error("Task runtime roots must not dirty the managed workspace.");
|
|
338
|
+
}
|
|
339
|
+
if (pathsOverlap(root, control.controllerSocketPath)) {
|
|
340
|
+
throw new Error("Task runtime roots overlap the Controller socket.");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (descriptor.serviceNamespace === control.tmuxNamespace) {
|
|
344
|
+
throw new Error("Task runtime service namespace overlaps the Controller tmux namespace.");
|
|
345
|
+
}
|
|
346
|
+
const declared = new Set(descriptor.externalCapabilities.declared);
|
|
347
|
+
const undeclared = descriptor.externalCapabilities.requested.find((capability) => !declared.has(capability));
|
|
348
|
+
if (undeclared !== undefined) {
|
|
349
|
+
throw new Error(`Task runtime external capability is undeclared: ${undeclared}.`);
|
|
350
|
+
}
|
|
351
|
+
const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
|
|
352
|
+
const resourceIds = new Set();
|
|
353
|
+
for (const resource of input.resources ?? []) {
|
|
354
|
+
validateObservation(resource);
|
|
355
|
+
if (resourceIds.has(resource.id)) {
|
|
356
|
+
throw new Error(`Task runtime resource inventory is ambiguous: ${resource.id}.`);
|
|
357
|
+
}
|
|
358
|
+
resourceIds.add(resource.id);
|
|
359
|
+
const exact = resource.ownership === "owned"
|
|
360
|
+
&& resource.descriptorFingerprint === fingerprint;
|
|
361
|
+
if (!exact) {
|
|
362
|
+
throw new Error(`Task runtime resource is ambiguous or externally owned: ${resource.id}.`);
|
|
363
|
+
}
|
|
364
|
+
if (resource.state === "unknown"
|
|
365
|
+
|| (resource.state === "active" && input.allowExactActive !== true)) {
|
|
366
|
+
throw new Error(`Task runtime resource is not safely reusable: ${resource.id}.`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return descriptor;
|
|
370
|
+
}
|
|
371
|
+
export function planTaskRuntimeCleanup(descriptor, reason, resources) {
|
|
372
|
+
requireCleanupReason(reason);
|
|
373
|
+
const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
|
|
374
|
+
const ids = new Set();
|
|
375
|
+
for (const resource of resources) {
|
|
376
|
+
validateObservation(resource);
|
|
377
|
+
if (ids.has(resource.id)) {
|
|
378
|
+
throw new Error(`Task runtime resource inventory is ambiguous: ${resource.id}.`);
|
|
379
|
+
}
|
|
380
|
+
ids.add(resource.id);
|
|
381
|
+
if (resource.ownership !== "owned"
|
|
382
|
+
|| resource.descriptorFingerprint !== fingerprint) {
|
|
383
|
+
throw new Error(`Task runtime cleanup refused an unowned resource: ${resource.id}.`);
|
|
384
|
+
}
|
|
385
|
+
if (resource.state !== "inactive") {
|
|
386
|
+
throw new Error(`Task runtime cleanup refused an active or unknown resource: ${resource.id}.`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return Object.freeze([...ids].sort());
|
|
390
|
+
}
|
|
391
|
+
function inspectGenerationRoot(descriptor, expectedFingerprint) {
|
|
392
|
+
const root = descriptor.roots.generation;
|
|
393
|
+
let metadata;
|
|
394
|
+
try {
|
|
395
|
+
metadata = lstatSync(root);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (isNodeCode(error, "ENOENT"))
|
|
399
|
+
return [];
|
|
400
|
+
return [{ id: root, kind: "directory", ownership: "ambiguous", state: "unknown" }];
|
|
401
|
+
}
|
|
402
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
403
|
+
return [{ id: root, kind: "directory", ownership: "ambiguous", state: "unknown" }];
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const markerPath = join(root, MARKER_FILE);
|
|
407
|
+
const markerMetadata = lstatSync(markerPath);
|
|
408
|
+
if (!markerMetadata.isFile() || markerMetadata.isSymbolicLink()) {
|
|
409
|
+
return [{ id: root, kind: "directory", ownership: "unmarked", state: "unknown" }];
|
|
410
|
+
}
|
|
411
|
+
const marker = parseMarker(readFileSync(markerPath, "utf8"));
|
|
412
|
+
const actualFingerprint = taskRuntimeIsolationFingerprint(marker.descriptor);
|
|
413
|
+
const exact = marker.fingerprint === actualFingerprint
|
|
414
|
+
&& actualFingerprint === expectedFingerprint
|
|
415
|
+
&& JSON.stringify(marker.descriptor) === JSON.stringify(descriptor);
|
|
416
|
+
return [{
|
|
417
|
+
id: root,
|
|
418
|
+
kind: "directory",
|
|
419
|
+
ownership: exact ? "owned" : "mismatched",
|
|
420
|
+
state: exact ? "inactive" : "unknown",
|
|
421
|
+
descriptorFingerprint: actualFingerprint
|
|
422
|
+
}];
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return [{ id: root, kind: "directory", ownership: "unmarked", state: "unknown" }];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function parseMarker(serialized) {
|
|
429
|
+
const value = JSON.parse(serialized);
|
|
430
|
+
if (!isRecord(value)
|
|
431
|
+
|| value.schemaVersion !== 1
|
|
432
|
+
|| value.kind !== "yui-task-runtime-resource-owner") {
|
|
433
|
+
throw new Error("Task runtime resource marker is invalid.");
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
schemaVersion: 1,
|
|
437
|
+
kind: "yui-task-runtime-resource-owner",
|
|
438
|
+
fingerprint: requireDigest(value.fingerprint),
|
|
439
|
+
descriptor: parseTaskRuntimeIsolationDescriptor(JSON.stringify(value.descriptor))
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function ensureOwnedDirectories(descriptor) {
|
|
443
|
+
for (const path of [
|
|
444
|
+
descriptor.roots.data,
|
|
445
|
+
descriptor.roots.cache,
|
|
446
|
+
descriptor.roots.temporary,
|
|
447
|
+
join(descriptor.roots.data, "state"),
|
|
448
|
+
join(descriptor.roots.temporary, "runtime")
|
|
449
|
+
]) {
|
|
450
|
+
ensureDirectoryChain(descriptor.roots.generation, path);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function ensureDirectoryChain(boundary, target) {
|
|
454
|
+
const root = resolve(boundary);
|
|
455
|
+
const destination = resolve(target);
|
|
456
|
+
if (root !== destination && !isWithin(root, destination)) {
|
|
457
|
+
throw new Error("Task runtime directory escaped its exact ownership boundary.");
|
|
458
|
+
}
|
|
459
|
+
const segments = relative(root, destination).split(/[\\/]/u).filter(Boolean);
|
|
460
|
+
let current = root;
|
|
461
|
+
ensureDirectory(current);
|
|
462
|
+
for (const segment of segments) {
|
|
463
|
+
current = join(current, segment);
|
|
464
|
+
ensureDirectory(current);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function ensureDirectory(path) {
|
|
468
|
+
try {
|
|
469
|
+
const metadata = lstatSync(path);
|
|
470
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
471
|
+
throw new Error(`Task runtime path is not an owned directory: ${path}.`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (!isNodeCode(error, "ENOENT"))
|
|
476
|
+
throw error;
|
|
477
|
+
mkdirSync(path, { mode: 0o700 });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function validatePreparation(preparation) {
|
|
481
|
+
const descriptor = parseTaskRuntimeIsolationDescriptor(JSON.stringify(preparation.descriptor));
|
|
482
|
+
const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
|
|
483
|
+
if (preparation.fingerprint !== fingerprint) {
|
|
484
|
+
throw new Error("Task runtime isolation preparation fingerprint changed.");
|
|
485
|
+
}
|
|
486
|
+
return { descriptor, fingerprint, environment: taskRuntimeIsolationEnvironment(descriptor) };
|
|
487
|
+
}
|
|
488
|
+
function taskRuntimeWorkspaceOwner(owner) {
|
|
489
|
+
return owner;
|
|
490
|
+
}
|
|
491
|
+
function taskRuntimeGenerationRoot(runtimeRoot, taskId, owner, launchId, pathLayout = "hierarchical") {
|
|
492
|
+
return join(taskRuntimeInventoryRoot(runtimeRoot, taskId, pathLayout), taskRuntimeOwnerDigest(taskId, owner, pathLayout), taskRuntimeLaunchDigest(taskId, launchId, pathLayout));
|
|
493
|
+
}
|
|
494
|
+
function taskRuntimeInventoryRoot(runtimeRoot, taskId, pathLayout) {
|
|
495
|
+
return pathLayout === "compact" ? runtimeRoot : join(runtimeRoot, taskId);
|
|
496
|
+
}
|
|
497
|
+
function taskRuntimeOwnerDigest(taskId, owner, pathLayout) {
|
|
498
|
+
return pathLayout === "compact"
|
|
499
|
+
? digest(JSON.stringify([taskId, owner])).slice(0, 20)
|
|
500
|
+
: digest(JSON.stringify(owner)).slice(0, 24);
|
|
501
|
+
}
|
|
502
|
+
function taskRuntimeLaunchDigest(taskId, launchId, pathLayout) {
|
|
503
|
+
return digest(JSON.stringify([taskId, launchId])).slice(0, pathLayout === "compact" ? 20 : 24);
|
|
504
|
+
}
|
|
505
|
+
function taskRuntimePathLayout(value) {
|
|
506
|
+
const pathLayout = value ?? "hierarchical";
|
|
507
|
+
if (pathLayout !== "hierarchical" && pathLayout !== "compact") {
|
|
508
|
+
throw new Error("Task runtime path layout is invalid.");
|
|
509
|
+
}
|
|
510
|
+
return pathLayout;
|
|
511
|
+
}
|
|
512
|
+
function taskRuntimeServiceNamespace(taskId, owner, launchId, generationId) {
|
|
513
|
+
return `yui-task-${digest(JSON.stringify([
|
|
514
|
+
taskId,
|
|
515
|
+
owner,
|
|
516
|
+
launchId,
|
|
517
|
+
generationId
|
|
518
|
+
])).slice(0, 24)}`;
|
|
519
|
+
}
|
|
520
|
+
function normalizeControlBoundary(value) {
|
|
521
|
+
return Object.freeze({
|
|
522
|
+
yuiHome: canonicalPath(value.yuiHome, "Control YUI_HOME"),
|
|
523
|
+
controllerSocketPath: canonicalPath(value.controllerSocketPath, "Controller socket path"),
|
|
524
|
+
tmuxNamespace: requireIdentity(value.tmuxNamespace, "Controller tmux namespace"),
|
|
525
|
+
globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path")))
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function validateObservation(resource) {
|
|
529
|
+
requireText(resource.id, "Task runtime resource id");
|
|
530
|
+
if (!["directory", "service", "port", "external"].includes(resource.kind)) {
|
|
531
|
+
throw new Error("Task runtime resource kind is invalid.");
|
|
532
|
+
}
|
|
533
|
+
if (!["owned", "unmarked", "mismatched", "ambiguous", "external"].includes(resource.ownership)) {
|
|
534
|
+
throw new Error("Task runtime resource ownership is invalid.");
|
|
535
|
+
}
|
|
536
|
+
if (!["inactive", "active", "unknown"].includes(resource.state)) {
|
|
537
|
+
throw new Error("Task runtime resource state is invalid.");
|
|
538
|
+
}
|
|
539
|
+
if (resource.descriptorFingerprint !== undefined) {
|
|
540
|
+
requireDigest(resource.descriptorFingerprint);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function allocations(value) {
|
|
544
|
+
if (!Array.isArray(value))
|
|
545
|
+
throw new Error("Task runtime port allocations are invalid.");
|
|
546
|
+
const names = new Set();
|
|
547
|
+
const selected = new Set();
|
|
548
|
+
const result = value.map((candidate) => {
|
|
549
|
+
if (!isRecord(candidate))
|
|
550
|
+
throw new Error("Task runtime port allocation is invalid.");
|
|
551
|
+
const name = requireIdentity(candidate.name, "Port allocation name");
|
|
552
|
+
const port = requirePort(candidate.port, "Port allocation");
|
|
553
|
+
if (names.has(name) || selected.has(port)) {
|
|
554
|
+
throw new Error("Task runtime port allocations must be unique.");
|
|
555
|
+
}
|
|
556
|
+
names.add(name);
|
|
557
|
+
selected.add(port);
|
|
558
|
+
return Object.freeze({ name, port });
|
|
559
|
+
});
|
|
560
|
+
return Object.freeze(result.sort((left, right) => left.name.localeCompare(right.name)));
|
|
561
|
+
}
|
|
562
|
+
function ports(value, label) {
|
|
563
|
+
if (!Array.isArray(value))
|
|
564
|
+
throw new Error(`${label} is invalid.`);
|
|
565
|
+
const result = value.map((port) => requirePort(port, label));
|
|
566
|
+
if (new Set(result).size !== result.length)
|
|
567
|
+
throw new Error(`${label} must be unique.`);
|
|
568
|
+
return Object.freeze([...result]);
|
|
569
|
+
}
|
|
570
|
+
function capabilities(value, label) {
|
|
571
|
+
if (!Array.isArray(value))
|
|
572
|
+
throw new Error(`${label} list is invalid.`);
|
|
573
|
+
const result = value.map((item) => requireIdentity(item, label)).sort();
|
|
574
|
+
if (new Set(result).size !== result.length)
|
|
575
|
+
throw new Error(`${label} must be unique.`);
|
|
576
|
+
return Object.freeze(result);
|
|
577
|
+
}
|
|
578
|
+
function requirePort(value, label) {
|
|
579
|
+
if (!Number.isInteger(value) || value < 1 || value > 65_535) {
|
|
580
|
+
throw new Error(`${label} is invalid.`);
|
|
581
|
+
}
|
|
582
|
+
return value;
|
|
583
|
+
}
|
|
584
|
+
function requireCleanupReason(value) {
|
|
585
|
+
if (!["failure", "timeout", "interruption", "completion", "reopen"].includes(value)) {
|
|
586
|
+
throw new Error("Task runtime cleanup reason is invalid.");
|
|
587
|
+
}
|
|
588
|
+
return value;
|
|
589
|
+
}
|
|
590
|
+
function canonicalPath(value, label) {
|
|
591
|
+
const path = requireText(value, label);
|
|
592
|
+
if (!isAbsolute(path))
|
|
593
|
+
throw new Error(`${label} must be absolute.`);
|
|
594
|
+
return resolve(path);
|
|
595
|
+
}
|
|
596
|
+
function pathsOverlap(left, right) {
|
|
597
|
+
return left === right || isWithin(left, right) || isWithin(right, left);
|
|
598
|
+
}
|
|
599
|
+
function isWithin(parent, child) {
|
|
600
|
+
const nested = relative(resolve(parent), resolve(child));
|
|
601
|
+
return nested.length > 0 && !nested.startsWith("..") && !isAbsolute(nested);
|
|
602
|
+
}
|
|
603
|
+
function requireIdentity(value, label) {
|
|
604
|
+
const identity = requireText(value, label);
|
|
605
|
+
if ([".", "..", "__proto__", "prototype", "constructor"].includes(identity)
|
|
606
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(identity)) {
|
|
607
|
+
throw new Error(`${label} is invalid.`);
|
|
608
|
+
}
|
|
609
|
+
return identity;
|
|
610
|
+
}
|
|
611
|
+
function requireText(value, label) {
|
|
612
|
+
if (typeof value !== "string"
|
|
613
|
+
|| value.length === 0
|
|
614
|
+
|| value.trim() !== value
|
|
615
|
+
|| value.includes("\0")) {
|
|
616
|
+
throw new Error(`${label} is invalid.`);
|
|
617
|
+
}
|
|
618
|
+
return value;
|
|
619
|
+
}
|
|
620
|
+
function requireDigest(value) {
|
|
621
|
+
const digestValue = requireText(value, "Task runtime fingerprint");
|
|
622
|
+
if (!/^[a-f0-9]{64}$/u.test(digestValue)) {
|
|
623
|
+
throw new Error("Task runtime fingerprint is invalid.");
|
|
624
|
+
}
|
|
625
|
+
return digestValue;
|
|
626
|
+
}
|
|
627
|
+
function requireRecord(value, label) {
|
|
628
|
+
if (!isRecord(value))
|
|
629
|
+
throw new Error(`${label} is invalid.`);
|
|
630
|
+
return value;
|
|
631
|
+
}
|
|
632
|
+
function isRecord(value) {
|
|
633
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
634
|
+
}
|
|
635
|
+
function digest(value) {
|
|
636
|
+
return createHash("sha256").update(value).digest("hex");
|
|
637
|
+
}
|
|
638
|
+
function isNodeCode(error, code) {
|
|
639
|
+
return typeof error === "object"
|
|
640
|
+
&& error !== null
|
|
641
|
+
&& "code" in error
|
|
642
|
+
&& error.code === code;
|
|
643
|
+
}
|