@devrouter/cli 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/devrouter-process +29 -2
- package/dist/devrouter-lifecycle-worker.js +1556 -427
- package/dist/devrouter.js +2954 -2333
- package/package.json +3 -2
- package/upgrade-prompts/0.5.5.md +35 -0
- package/upgrade-prompts/0.5.6.md +73 -0
|
@@ -23,8 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/core/devpod-exec.ts
|
|
26
|
-
var
|
|
27
|
-
var
|
|
26
|
+
var import_node_child_process33 = require("child_process");
|
|
27
|
+
var import_node_crypto24 = require("crypto");
|
|
28
28
|
|
|
29
29
|
// src/core/devpod-environment.ts
|
|
30
30
|
var import_node_child_process3 = require("child_process");
|
|
@@ -1982,7 +1982,7 @@ function selectDevpodWorkspace(workspaces, repoPath) {
|
|
|
1982
1982
|
}
|
|
1983
1983
|
|
|
1984
1984
|
// src/core/devsy-exec.ts
|
|
1985
|
-
var
|
|
1985
|
+
var import_node_child_process32 = require("child_process");
|
|
1986
1986
|
|
|
1987
1987
|
// src/core/devsy-exec-proof.ts
|
|
1988
1988
|
var import_node_fs7 = __toESM(require("fs"));
|
|
@@ -2924,12 +2924,10 @@ function buildHostRoutesDocument(routes, tlsEnabled) {
|
|
|
2924
2924
|
}
|
|
2925
2925
|
};
|
|
2926
2926
|
}
|
|
2927
|
-
const document = {
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
}
|
|
2932
|
-
};
|
|
2927
|
+
const document = {};
|
|
2928
|
+
if (Object.keys(routers).length > 0) {
|
|
2929
|
+
document.http = { routers, services };
|
|
2930
|
+
}
|
|
2933
2931
|
if (Object.keys(tcpRouters).length > 0) {
|
|
2934
2932
|
document.tcp = { routers: tcpRouters, services: tcpServices };
|
|
2935
2933
|
}
|
|
@@ -3065,7 +3063,11 @@ function parseCanonicalState(raw) {
|
|
|
3065
3063
|
throw new Error(`Canonical host-route document is invalid: ${error.message}`);
|
|
3066
3064
|
}
|
|
3067
3065
|
const expectedDocument = buildHostRoutesDocument(routes, metadata.tlsEnabled);
|
|
3068
|
-
|
|
3066
|
+
const matchesLegacyEmptyHttp = expectedDocument.http === void 0 && (0, import_node_util2.isDeepStrictEqual)(document, {
|
|
3067
|
+
...expectedDocument,
|
|
3068
|
+
http: { routers: {}, services: {} }
|
|
3069
|
+
});
|
|
3070
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(document, expectedDocument) && !matchesLegacyEmptyHttp) {
|
|
3069
3071
|
throw new Error("Canonical host-route document does not match its metadata.");
|
|
3070
3072
|
}
|
|
3071
3073
|
return { kind: "canonical", metadata };
|
|
@@ -3388,6 +3390,12 @@ function validateState(value, repoPath, workspace) {
|
|
|
3388
3390
|
if (typeof candidate.profile !== "string" || candidate.profile.length === 0) {
|
|
3389
3391
|
throw new Error("Managed runtime state has no canonical profile.");
|
|
3390
3392
|
}
|
|
3393
|
+
if (candidate.variant !== void 0 && (typeof candidate.variant !== "string" || candidate.variant.length > 64 || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(candidate.variant))) {
|
|
3394
|
+
throw new Error("Managed runtime state has an invalid runtime variant.");
|
|
3395
|
+
}
|
|
3396
|
+
if (candidate.hostPortDeclarationsSha256 !== void 0 && (typeof candidate.hostPortDeclarationsSha256 !== "string" || !/^[a-f0-9]{64}$/.test(candidate.hostPortDeclarationsSha256))) {
|
|
3397
|
+
throw new Error("Managed runtime state has an invalid host-port declaration fingerprint.");
|
|
3398
|
+
}
|
|
3391
3399
|
if (!candidate.desired || typeof candidate.desired !== "object") {
|
|
3392
3400
|
throw new Error("Managed runtime state has no desired resource set.");
|
|
3393
3401
|
}
|
|
@@ -3414,6 +3422,8 @@ function validateState(value, repoPath, workspace) {
|
|
|
3414
3422
|
devpodId: candidate.devpodId,
|
|
3415
3423
|
composeProject: candidate.composeProject,
|
|
3416
3424
|
profile: candidate.profile,
|
|
3425
|
+
...candidate.variant !== void 0 ? { variant: candidate.variant } : {},
|
|
3426
|
+
...candidate.hostPortDeclarationsSha256 !== void 0 ? { hostPortDeclarationsSha256: candidate.hostPortDeclarationsSha256 } : {},
|
|
3417
3427
|
desired: {
|
|
3418
3428
|
apps: [...desired.apps],
|
|
3419
3429
|
services: [...desired.services],
|
|
@@ -4053,6 +4063,89 @@ function captureManagedStopBaseline(state, plan, primaryId, expectedEndpoint) {
|
|
|
4053
4063
|
prove(state, baseline);
|
|
4054
4064
|
return baseline;
|
|
4055
4065
|
}
|
|
4066
|
+
function recoverManagedStopBaseline(state, readPlan, beforePersist) {
|
|
4067
|
+
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
4068
|
+
const plan = readPlan();
|
|
4069
|
+
const owner = registration(state);
|
|
4070
|
+
if (Object.entries(owner).some(
|
|
4071
|
+
([key, value]) => baseline[key] !== value
|
|
4072
|
+
) || baseline.sourceContainer)
|
|
4073
|
+
throw new Error("Stop recovery requires the unchanged provider registration.");
|
|
4074
|
+
const observe = () => {
|
|
4075
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state) || !(0, import_node_util3.isDeepStrictEqual)(registration(state), owner) || !(0, import_node_util3.isDeepStrictEqual)(readPlan(), plan) || resolveManagedStopEndpoint() !== baseline.endpoint || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
|
|
4076
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
|
|
4077
|
+
))
|
|
4078
|
+
throw new Error("Stop recovery authority changed.");
|
|
4079
|
+
const currentOwner = inspectDevsyWorkspaceOwnership(
|
|
4080
|
+
listDevsyWorkspaces(),
|
|
4081
|
+
state.devpodId,
|
|
4082
|
+
state.repoPath
|
|
4083
|
+
);
|
|
4084
|
+
if (currentOwner.status !== "owned") throw new Error("Stop recovery owner is unavailable.");
|
|
4085
|
+
proveLocalDockerSelection(currentOwner.workspace);
|
|
4086
|
+
assertManagedStopContainersAbsent(
|
|
4087
|
+
baseline.endpoint,
|
|
4088
|
+
baseline.containers.map((container) => container.id)
|
|
4089
|
+
);
|
|
4090
|
+
const containers = inspectManagedStopContainers(baseline.project, baseline.endpoint);
|
|
4091
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(
|
|
4092
|
+
containers.map((container) => container.labels["com.docker.compose.service"]).sort(),
|
|
4093
|
+
[...plan.desiredServices].sort()
|
|
4094
|
+
))
|
|
4095
|
+
throw new Error("Stop recovery requires the complete selected service population.");
|
|
4096
|
+
const primary2 = containers.find(
|
|
4097
|
+
(container) => container.labels["com.docker.compose.service"] === plan.primaryService
|
|
4098
|
+
);
|
|
4099
|
+
if (!primary2 || !(0, import_node_util3.isDeepStrictEqual)(
|
|
4100
|
+
inspectProviderRunnerContainers(
|
|
4101
|
+
baseline.endpoint,
|
|
4102
|
+
managedRunnerId(baseline.uid, baseline.providerId)
|
|
4103
|
+
).sort(),
|
|
4104
|
+
[primary2.id]
|
|
4105
|
+
))
|
|
4106
|
+
throw new Error("Stop recovery provider runner population changed.");
|
|
4107
|
+
assertManagedContainerConfigUnchanged({
|
|
4108
|
+
plan,
|
|
4109
|
+
containers,
|
|
4110
|
+
workspace: state.workspace ? {
|
|
4111
|
+
token: state.workspace,
|
|
4112
|
+
gitCommonDir: resolveGitCommonDir(state.repoPath)
|
|
4113
|
+
} : void 0
|
|
4114
|
+
});
|
|
4115
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(registration(state), owner) || !(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state) || !(0, import_node_util3.isDeepStrictEqual)(readPlan(), plan) || resolveManagedStopEndpoint() !== baseline.endpoint || inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
4116
|
+
throw new Error("Stop recovery authority changed during configuration inspection.");
|
|
4117
|
+
return containers;
|
|
4118
|
+
};
|
|
4119
|
+
const first = observe();
|
|
4120
|
+
const candidate = {
|
|
4121
|
+
...state,
|
|
4122
|
+
sourceConfigSha256: plan.sourceConfigSha256,
|
|
4123
|
+
effectiveConfigSha256: plan.effectiveConfigSha256,
|
|
4124
|
+
status: "degraded",
|
|
4125
|
+
transitionPhase: "stop-recovery",
|
|
4126
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4127
|
+
stopBaseline: void 0
|
|
4128
|
+
};
|
|
4129
|
+
const primary = first.find(
|
|
4130
|
+
(container) => container.labels["com.docker.compose.service"] === plan.primaryService
|
|
4131
|
+
);
|
|
4132
|
+
if (!primary) throw new Error("Stop recovery primary is unavailable.");
|
|
4133
|
+
candidate.stopBaseline = captureManagedStopBaseline(
|
|
4134
|
+
candidate,
|
|
4135
|
+
plan,
|
|
4136
|
+
primary.id,
|
|
4137
|
+
baseline.endpoint
|
|
4138
|
+
);
|
|
4139
|
+
if (!(0, import_node_util3.isDeepStrictEqual)(
|
|
4140
|
+
first.map(identity).sort((a, b) => compareText(a.id, b.id)),
|
|
4141
|
+
observe().map(identity).sort((a, b) => compareText(a.id, b.id))
|
|
4142
|
+
))
|
|
4143
|
+
throw new Error("Stop recovery population changed during inspection.");
|
|
4144
|
+
beforePersist();
|
|
4145
|
+
claimLifecycleEffect();
|
|
4146
|
+
writeManagedRuntimeState(candidate);
|
|
4147
|
+
return candidate;
|
|
4148
|
+
}
|
|
4056
4149
|
function proveRetainedManagedStop(state) {
|
|
4057
4150
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
4058
4151
|
if (!(0, import_node_util3.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state)) {
|
|
@@ -5397,7 +5490,7 @@ function retain(session, previous) {
|
|
|
5397
5490
|
|
|
5398
5491
|
// src/core/reliability-operation-store.ts
|
|
5399
5492
|
function installedCliVersion() {
|
|
5400
|
-
return true ? "0.5.
|
|
5493
|
+
return true ? "0.5.6" : "0.0.0-dev";
|
|
5401
5494
|
}
|
|
5402
5495
|
function compareCliVersions(left, right) {
|
|
5403
5496
|
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -6156,6 +6249,9 @@ var MAX_CAPACITY_PROFILES = 256;
|
|
|
6156
6249
|
var MAX_CAPACITY_OPERATIONS = 64;
|
|
6157
6250
|
var MAX_CAPACITY_TRANSITIONS = 1024;
|
|
6158
6251
|
var MAX_CAPACITY_NAME_LENGTH = 64;
|
|
6252
|
+
var MAX_RUNTIME_VARIANTS = 32;
|
|
6253
|
+
var MAX_RUNTIME_VARIANT_NAME_LENGTH = 64;
|
|
6254
|
+
var RUNTIME_VARIANT_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6159
6255
|
var UPSTREAM_TEMPLATE_RE = /^(?:\$\{WORKSPACE\}|[a-zA-Z0-9._-])+:\d{1,5}$/;
|
|
6160
6256
|
function assertUpstreamSpec(upstream, label) {
|
|
6161
6257
|
if (upstream.includes(WORKSPACE_PLACEHOLDER)) {
|
|
@@ -6632,6 +6728,53 @@ function parsePrepareCommand(value, pathLabel) {
|
|
|
6632
6728
|
return item;
|
|
6633
6729
|
});
|
|
6634
6730
|
}
|
|
6731
|
+
function assertRuntimeVariantName(value, pathLabel) {
|
|
6732
|
+
if (value.length === 0 || value.length > MAX_RUNTIME_VARIANT_NAME_LENGTH || !RUNTIME_VARIANT_NAME_RE.test(value)) {
|
|
6733
|
+
throw new Error(
|
|
6734
|
+
`${pathLabel} must use lowercase letters or digits separated by single hyphens, at most ${MAX_RUNTIME_VARIANT_NAME_LENGTH} characters.`
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6737
|
+
if (value === "default") {
|
|
6738
|
+
throw new Error(`${pathLabel} 'default' is implicit and reserved.`);
|
|
6739
|
+
}
|
|
6740
|
+
}
|
|
6741
|
+
function parseRuntimeVariants(value, pathLabel) {
|
|
6742
|
+
if (value === void 0) {
|
|
6743
|
+
return void 0;
|
|
6744
|
+
}
|
|
6745
|
+
if (!Array.isArray(value)) {
|
|
6746
|
+
throw new Error(`${pathLabel} must be an array of strings.`);
|
|
6747
|
+
}
|
|
6748
|
+
if (value.length > MAX_RUNTIME_VARIANTS) {
|
|
6749
|
+
throw new Error(`${pathLabel} accepts at most ${MAX_RUNTIME_VARIANTS} additional names.`);
|
|
6750
|
+
}
|
|
6751
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6752
|
+
return value.map((item, index) => {
|
|
6753
|
+
const itemPath = `${pathLabel}[${index}]`;
|
|
6754
|
+
if (typeof item !== "string") {
|
|
6755
|
+
throw new Error(`${itemPath} must be a string.`);
|
|
6756
|
+
}
|
|
6757
|
+
assertRuntimeVariantName(item, itemPath);
|
|
6758
|
+
if (seen.has(item)) {
|
|
6759
|
+
throw new Error(`${pathLabel} contains duplicate '${item}'.`);
|
|
6760
|
+
}
|
|
6761
|
+
seen.add(item);
|
|
6762
|
+
return item;
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6765
|
+
function resolveRuntimeVariant(config, requested) {
|
|
6766
|
+
if (requested === void 0) {
|
|
6767
|
+
return "default";
|
|
6768
|
+
}
|
|
6769
|
+
if (requested === "default") {
|
|
6770
|
+
return requested;
|
|
6771
|
+
}
|
|
6772
|
+
assertRuntimeVariantName(requested, "Runtime variant");
|
|
6773
|
+
if (!config.managedRuntime?.variants?.includes(requested)) {
|
|
6774
|
+
throw new Error(`Runtime variant '${requested}' is not declared in managedRuntime.variants.`);
|
|
6775
|
+
}
|
|
6776
|
+
return requested;
|
|
6777
|
+
}
|
|
6635
6778
|
function parseManagedRuntime(value, configPath) {
|
|
6636
6779
|
if (value === void 0) {
|
|
6637
6780
|
return void 0;
|
|
@@ -6639,9 +6782,13 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6639
6782
|
const managedRuntime = ensureObject(value, `${configPath}.managedRuntime`);
|
|
6640
6783
|
ensureAllowedKeys(
|
|
6641
6784
|
managedRuntime,
|
|
6642
|
-
["devcontainer", "processes", "network"],
|
|
6785
|
+
["devcontainer", "processes", "network", "variants"],
|
|
6643
6786
|
`${configPath}.managedRuntime`
|
|
6644
6787
|
);
|
|
6788
|
+
const variants = parseRuntimeVariants(
|
|
6789
|
+
managedRuntime.variants,
|
|
6790
|
+
`${configPath}.managedRuntime.variants`
|
|
6791
|
+
);
|
|
6645
6792
|
let network;
|
|
6646
6793
|
if (managedRuntime.network !== void 0) {
|
|
6647
6794
|
const value2 = ensureObject(managedRuntime.network, `${configPath}.managedRuntime.network`);
|
|
@@ -6667,7 +6814,7 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6667
6814
|
);
|
|
6668
6815
|
ensureAllowedKeys(
|
|
6669
6816
|
devcontainer,
|
|
6670
|
-
["baseServices", "profileServices", "prepareCommand"],
|
|
6817
|
+
["baseServices", "profileServices", "prepareCommand", "hostPorts"],
|
|
6671
6818
|
`${configPath}.managedRuntime.devcontainer`
|
|
6672
6819
|
);
|
|
6673
6820
|
const baseServices = parseRequiredUniqueStringArray(
|
|
@@ -6689,6 +6836,41 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6689
6836
|
`${configPath}.managedRuntime.devcontainer.baseServices and profileServices overlap: ${overlappingServices.join(", ")}.`
|
|
6690
6837
|
);
|
|
6691
6838
|
}
|
|
6839
|
+
let hostPorts;
|
|
6840
|
+
if (devcontainer.hostPorts !== void 0) {
|
|
6841
|
+
const ports = ensureObject(
|
|
6842
|
+
devcontainer.hostPorts,
|
|
6843
|
+
`${configPath}.managedRuntime.devcontainer.hostPorts`
|
|
6844
|
+
);
|
|
6845
|
+
if (Object.keys(ports).length > 32)
|
|
6846
|
+
throw new Error("managedRuntime.devcontainer.hostPorts accepts at most 32 declarations.");
|
|
6847
|
+
hostPorts = {};
|
|
6848
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
6849
|
+
const registered = /* @__PURE__ */ new Set([...baseServices, ...profileServices]);
|
|
6850
|
+
for (const [name, rawPort] of Object.entries(ports)) {
|
|
6851
|
+
if (name.length > 64 || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name))
|
|
6852
|
+
throw new Error(
|
|
6853
|
+
"Host-port names must be lowercase kebab-case identifiers of at most 64 characters."
|
|
6854
|
+
);
|
|
6855
|
+
const port = ensureObject(rawPort, `managedRuntime.devcontainer.hostPorts.${name}`);
|
|
6856
|
+
ensureAllowedKeys(
|
|
6857
|
+
port,
|
|
6858
|
+
["service", "target"],
|
|
6859
|
+
`managedRuntime.devcontainer.hostPorts.${name}`
|
|
6860
|
+
);
|
|
6861
|
+
if (typeof port.service !== "string" || !registered.has(port.service))
|
|
6862
|
+
throw new Error(`Host port '${name}' must reference a registered base or profile service.`);
|
|
6863
|
+
if (!Number.isInteger(port.target) || port.target < 1 || port.target > 65535)
|
|
6864
|
+
throw new Error(`Host port '${name}' target must be an integer from 1 to 65535.`);
|
|
6865
|
+
const pair = `${port.service}:${port.target}`;
|
|
6866
|
+
if (pairs.has(pair)) throw new Error(`Host port '${name}' duplicates a service and target.`);
|
|
6867
|
+
pairs.add(pair);
|
|
6868
|
+
Object.defineProperty(hostPorts, name, {
|
|
6869
|
+
value: { service: port.service, target: port.target },
|
|
6870
|
+
enumerable: true
|
|
6871
|
+
});
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6692
6874
|
const processes = parseRequiredUniqueStringArray(
|
|
6693
6875
|
managedRuntime.processes,
|
|
6694
6876
|
`${configPath}.managedRuntime.processes`
|
|
@@ -6703,10 +6885,12 @@ function parseManagedRuntime(value, configPath) {
|
|
|
6703
6885
|
}
|
|
6704
6886
|
return {
|
|
6705
6887
|
...network ? { network } : {},
|
|
6888
|
+
...variants !== void 0 ? { variants } : {},
|
|
6706
6889
|
devcontainer: {
|
|
6707
6890
|
baseServices,
|
|
6708
6891
|
profileServices,
|
|
6709
|
-
...prepareCommand ? { prepareCommand } : {}
|
|
6892
|
+
...prepareCommand ? { prepareCommand } : {},
|
|
6893
|
+
...hostPorts !== void 0 ? { hostPorts } : {}
|
|
6710
6894
|
},
|
|
6711
6895
|
processes
|
|
6712
6896
|
};
|
|
@@ -7407,7 +7591,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs19.default.rea
|
|
|
7407
7591
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
7408
7592
|
const requiredVersion = config.devrouter?.version;
|
|
7409
7593
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
7410
|
-
const cliVersion = true ? "0.5.
|
|
7594
|
+
const cliVersion = true ? "0.5.6" : "0.0.0-dev";
|
|
7411
7595
|
if (cliVersion !== "0.0.0-dev" && compareSemver2(requiredVersion, cliVersion) > 0) {
|
|
7412
7596
|
hasWarnedVersionMismatch = true;
|
|
7413
7597
|
process.stderr.write(
|
|
@@ -10464,147 +10648,1167 @@ var ExecutionOutcomeError = class extends Error {
|
|
|
10464
10648
|
};
|
|
10465
10649
|
|
|
10466
10650
|
// src/core/reliability-lifecycle.ts
|
|
10467
|
-
var
|
|
10651
|
+
var import_node_crypto23 = require("crypto");
|
|
10468
10652
|
var import_node_path41 = __toESM(require("path"));
|
|
10469
10653
|
var import_node_util7 = require("util");
|
|
10470
10654
|
|
|
10471
10655
|
// src/core/capacity-policy.ts
|
|
10472
10656
|
var import_node_fs27 = __toESM(require("fs"));
|
|
10473
10657
|
var import_node_path31 = __toESM(require("path"));
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
var
|
|
10477
|
-
var
|
|
10478
|
-
var
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
);
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
function reliabilityAttention(state) {
|
|
10496
|
-
assertReliabilityState(state);
|
|
10497
|
-
const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
10498
|
-
if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
|
|
10499
|
-
if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
|
|
10500
|
-
return "operation-unknown";
|
|
10501
|
-
if (state.desired === "parked-for-capacity")
|
|
10502
|
-
return completeStop ? "capacity-parked" : "stop-incomplete";
|
|
10503
|
-
if (state.admission === "denied-unadmittable") return "unadmittable";
|
|
10504
|
-
if (state.admission === "waiting") return "capacity-waiting";
|
|
10505
|
-
if (state.phase === "recovering") return "recovering";
|
|
10506
|
-
if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
|
|
10507
|
-
return "starting";
|
|
10508
|
-
return void 0;
|
|
10509
|
-
}
|
|
10510
|
-
|
|
10511
|
-
// src/core/recovery-budget.ts
|
|
10512
|
-
function findRecoveryUnit(incident, unit) {
|
|
10513
|
-
return (incident.units ?? []).find((record5) => record5.key === unit.key);
|
|
10514
|
-
}
|
|
10515
|
-
function restartsFor(kind, limits) {
|
|
10516
|
-
return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
|
|
10517
|
-
}
|
|
10518
|
-
function recoveryBudgetRefusal(input) {
|
|
10519
|
-
const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
|
|
10520
|
-
const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
|
|
10521
|
-
if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
|
|
10522
|
-
if (input.unit !== void 0) {
|
|
10523
|
-
const record5 = findRecoveryUnit(input.incident, input.unit);
|
|
10524
|
-
if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
|
|
10525
|
-
return "unit-exhausted";
|
|
10658
|
+
var MAX_DOMAINS = 256;
|
|
10659
|
+
var MAX_ENROLLMENTS = 256;
|
|
10660
|
+
var MAX_PROFILES_PER_ENROLLMENT = 64;
|
|
10661
|
+
var MAX_STRING_LENGTH = 256;
|
|
10662
|
+
var MAX_PATH_LENGTH = 4096;
|
|
10663
|
+
function readCapacityPolicy(directory) {
|
|
10664
|
+
const absolute2 = import_node_path31.default.resolve(directory);
|
|
10665
|
+
let current = import_node_path31.default.parse(absolute2).root;
|
|
10666
|
+
for (const part of absolute2.slice(current.length).split(import_node_path31.default.sep)) {
|
|
10667
|
+
current = import_node_path31.default.join(current, part);
|
|
10668
|
+
let stat;
|
|
10669
|
+
try {
|
|
10670
|
+
stat = import_node_fs27.default.lstatSync(current);
|
|
10671
|
+
} catch (error) {
|
|
10672
|
+
if (error.code === "ENOENT") return void 0;
|
|
10673
|
+
throw error;
|
|
10674
|
+
}
|
|
10675
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
10676
|
+
throw new Error("Unsafe capacity policy directory.");
|
|
10677
|
+
if (current === absolute2 && (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0))
|
|
10678
|
+
throw new Error("Capacity policy directory is not private.");
|
|
10526
10679
|
}
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10680
|
+
let descriptor;
|
|
10681
|
+
try {
|
|
10682
|
+
descriptor = import_node_fs27.default.openSync(
|
|
10683
|
+
import_node_path31.default.join(absolute2, "capacity-policy.json"),
|
|
10684
|
+
import_node_fs27.default.constants.O_RDONLY | import_node_fs27.default.constants.O_NOFOLLOW | import_node_fs27.default.constants.O_NONBLOCK
|
|
10685
|
+
);
|
|
10686
|
+
} catch (error) {
|
|
10687
|
+
if (error.code === "ENOENT") return void 0;
|
|
10688
|
+
throw error;
|
|
10689
|
+
}
|
|
10690
|
+
try {
|
|
10691
|
+
const stat = import_node_fs27.default.fstatSync(descriptor);
|
|
10692
|
+
const limit = 1048576;
|
|
10693
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0 || stat.size > limit)
|
|
10694
|
+
throw new Error("Capacity policy is not a bounded private file.");
|
|
10695
|
+
const bytes2 = Buffer.alloc(limit + 1);
|
|
10696
|
+
const count = import_node_fs27.default.readSync(descriptor, bytes2, 0, bytes2.length, 0);
|
|
10697
|
+
if (count > limit) throw new Error("Capacity policy exceeds byte limit.");
|
|
10698
|
+
return parseCapacityPolicy(JSON.parse(bytes2.subarray(0, count).toString("utf8")));
|
|
10699
|
+
} finally {
|
|
10700
|
+
import_node_fs27.default.closeSync(descriptor);
|
|
10701
|
+
}
|
|
10702
|
+
}
|
|
10703
|
+
var DOMAIN_ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10704
|
+
var PROFILE_NAME_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10705
|
+
var WORKSPACE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10706
|
+
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
10707
|
+
var DEFAULT_CAPACITY_RECOVERY = {
|
|
10708
|
+
enabled: false,
|
|
10709
|
+
maxProcessRestarts: 2,
|
|
10710
|
+
maxServiceRestarts: 1,
|
|
10711
|
+
maxCorrectiveActions: 3,
|
|
10712
|
+
windowSeconds: 600,
|
|
10713
|
+
observationSeconds: 30,
|
|
10714
|
+
resumeDwellSeconds: 300
|
|
10715
|
+
};
|
|
10716
|
+
function ensureObject2(value, label) {
|
|
10717
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10718
|
+
throw new Error(`${label} must be an object.`);
|
|
10719
|
+
}
|
|
10720
|
+
return value;
|
|
10530
10721
|
}
|
|
10531
|
-
function
|
|
10532
|
-
const
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
const index = units.findIndex((record5) => record5.key === input.unit?.key);
|
|
10537
|
-
if (index === -1) {
|
|
10538
|
-
if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
|
|
10539
|
-
units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
|
|
10540
|
-
} else {
|
|
10541
|
-
const record5 = units[index];
|
|
10542
|
-
units[index] = {
|
|
10543
|
-
...record5,
|
|
10544
|
-
actions: record5.actions + 1,
|
|
10545
|
-
lastActionAtMs: input.nowMs
|
|
10546
|
-
};
|
|
10722
|
+
function ensureAllowedKeys2(value, allowedKeys, label) {
|
|
10723
|
+
const allowed = new Set(allowedKeys);
|
|
10724
|
+
for (const key of Object.keys(value)) {
|
|
10725
|
+
if (!allowed.has(key)) {
|
|
10726
|
+
throw new Error(`${label}.${key} is not supported.`);
|
|
10547
10727
|
}
|
|
10548
10728
|
}
|
|
10549
|
-
return {
|
|
10550
|
-
ok: true,
|
|
10551
|
-
incident: {
|
|
10552
|
-
...input.incident,
|
|
10553
|
-
startedAtMs: input.incident.startedAtMs ?? input.nowMs,
|
|
10554
|
-
correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
|
|
10555
|
-
units
|
|
10556
|
-
}
|
|
10557
|
-
};
|
|
10558
10729
|
}
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10730
|
+
function parseBoundedString(value, label, maxLength = MAX_STRING_LENGTH) {
|
|
10731
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
10732
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
10733
|
+
}
|
|
10734
|
+
if (value !== value.trim()) {
|
|
10735
|
+
throw new Error(`${label} must not have leading or trailing whitespace.`);
|
|
10736
|
+
}
|
|
10737
|
+
if (value.length > maxLength) {
|
|
10738
|
+
throw new Error(`${label} exceeds the maximum length of ${maxLength} characters.`);
|
|
10739
|
+
}
|
|
10740
|
+
if ([...value].some(
|
|
10741
|
+
(character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127
|
|
10742
|
+
)) {
|
|
10743
|
+
throw new Error(`${label} must not contain control characters.`);
|
|
10744
|
+
}
|
|
10745
|
+
return value;
|
|
10566
10746
|
}
|
|
10567
|
-
function
|
|
10568
|
-
|
|
10747
|
+
function parseSafeInteger(value, label, minimum) {
|
|
10748
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || Object.is(value, -0)) {
|
|
10749
|
+
throw new Error(`${label} must be a safe integer.`);
|
|
10750
|
+
}
|
|
10751
|
+
if (value < minimum) {
|
|
10752
|
+
throw new Error(`${label} must be at least ${minimum}.`);
|
|
10753
|
+
}
|
|
10754
|
+
return value;
|
|
10569
10755
|
}
|
|
10570
|
-
function
|
|
10571
|
-
return
|
|
10756
|
+
function parsePositiveInteger(value, label) {
|
|
10757
|
+
return parseSafeInteger(value, label, 1);
|
|
10572
10758
|
}
|
|
10573
|
-
function
|
|
10574
|
-
|
|
10759
|
+
function parseCanonicalAbsolutePath(value, label) {
|
|
10760
|
+
const parsed = parseBoundedString(value, label, MAX_PATH_LENGTH);
|
|
10761
|
+
if (!import_node_path31.default.posix.isAbsolute(parsed)) {
|
|
10762
|
+
throw new Error(`${label} must be an absolute path.`);
|
|
10763
|
+
}
|
|
10764
|
+
if (import_node_path31.default.posix.resolve(parsed) !== parsed) {
|
|
10765
|
+
throw new Error(`${label} must be a canonical absolute path.`);
|
|
10766
|
+
}
|
|
10767
|
+
return parsed;
|
|
10575
10768
|
}
|
|
10576
|
-
function
|
|
10577
|
-
|
|
10769
|
+
function parseDomainId(value, label) {
|
|
10770
|
+
const id5 = parseBoundedString(value, label, 64);
|
|
10771
|
+
if (!DOMAIN_ID_RE.test(id5)) {
|
|
10772
|
+
throw new Error(`${label} must be a lowercase alphanumeric or hyphen domain ID.`);
|
|
10773
|
+
}
|
|
10774
|
+
return id5;
|
|
10578
10775
|
}
|
|
10579
|
-
function
|
|
10580
|
-
|
|
10776
|
+
function parseProfileName(value, label) {
|
|
10777
|
+
const combination = parseBoundedString(value, label, 64);
|
|
10778
|
+
const names = combination.split(",");
|
|
10779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10780
|
+
for (const [index, name] of names.entries()) {
|
|
10781
|
+
if (!PROFILE_NAME_RE2.test(name)) {
|
|
10782
|
+
throw new Error(
|
|
10783
|
+
`${label}[${index}] must be a lowercase alphanumeric or hyphen profile name.`
|
|
10784
|
+
);
|
|
10785
|
+
}
|
|
10786
|
+
if (seen.has(name)) {
|
|
10787
|
+
throw new Error(`${label} contains duplicate profile '${name}'.`);
|
|
10788
|
+
}
|
|
10789
|
+
seen.add(name);
|
|
10790
|
+
}
|
|
10791
|
+
return [...names].sort().join(",");
|
|
10581
10792
|
}
|
|
10582
|
-
function
|
|
10583
|
-
if (!
|
|
10584
|
-
throw new Error(
|
|
10793
|
+
function parseUniqueProfiles(value, label) {
|
|
10794
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
10795
|
+
throw new Error(`${label} must be a non-empty array of profile names.`);
|
|
10585
10796
|
}
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10797
|
+
if (value.length > MAX_PROFILES_PER_ENROLLMENT) {
|
|
10798
|
+
throw new Error(
|
|
10799
|
+
`${label} exceeds the maximum of ${MAX_PROFILES_PER_ENROLLMENT} profile names.`
|
|
10800
|
+
);
|
|
10801
|
+
}
|
|
10802
|
+
const profiles = [];
|
|
10803
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10804
|
+
for (const [index, entry] of value.entries()) {
|
|
10805
|
+
const profile = parseProfileName(entry, `${label}[${index}]`);
|
|
10806
|
+
if (seen.has(profile)) {
|
|
10807
|
+
throw new Error(`${label} contains duplicate or aliased profile combination '${profile}'.`);
|
|
10808
|
+
}
|
|
10809
|
+
seen.add(profile);
|
|
10810
|
+
profiles.push(profile);
|
|
10811
|
+
}
|
|
10812
|
+
return profiles;
|
|
10813
|
+
}
|
|
10814
|
+
function parseDigest(value, label) {
|
|
10815
|
+
const digest = parseBoundedString(value, label, 64);
|
|
10816
|
+
if (!SHA256_RE.test(digest)) {
|
|
10817
|
+
throw new Error(`${label} must be a lowercase SHA-256 hex digest.`);
|
|
10818
|
+
}
|
|
10819
|
+
return digest;
|
|
10820
|
+
}
|
|
10821
|
+
function parseDomain(value, domainId) {
|
|
10822
|
+
const label = `domains.${domainId}`;
|
|
10823
|
+
const domain = ensureObject2(value, label);
|
|
10824
|
+
const kind = parseBoundedString(domain.kind, `${label}.kind`);
|
|
10825
|
+
if (kind === "host") {
|
|
10826
|
+
ensureAllowedKeys2(
|
|
10827
|
+
domain,
|
|
10828
|
+
[
|
|
10829
|
+
"kind",
|
|
10830
|
+
"adapter",
|
|
10831
|
+
"capacityBytes",
|
|
10832
|
+
"protectedHeadroomBytes",
|
|
10833
|
+
"unmanagedAllowanceBytes",
|
|
10834
|
+
"startupSlots",
|
|
10835
|
+
"heavySlots"
|
|
10836
|
+
],
|
|
10837
|
+
label
|
|
10838
|
+
);
|
|
10839
|
+
const adapter = parseBoundedString(domain.adapter, `${label}.adapter`);
|
|
10840
|
+
if (adapter !== "macos-host-v1" && adapter !== "macos-declared-v1") {
|
|
10841
|
+
throw new Error(`${label}.adapter must be 'macos-host-v1' or 'macos-declared-v1'.`);
|
|
10842
|
+
}
|
|
10843
|
+
const capacityBytes = parsePositiveInteger(domain.capacityBytes, `${label}.capacityBytes`);
|
|
10844
|
+
const protectedHeadroomBytes = parsePositiveInteger(
|
|
10845
|
+
domain.protectedHeadroomBytes,
|
|
10846
|
+
`${label}.protectedHeadroomBytes`
|
|
10847
|
+
);
|
|
10848
|
+
if (protectedHeadroomBytes >= capacityBytes) {
|
|
10849
|
+
throw new Error(`${label}.protectedHeadroomBytes must be less than capacityBytes.`);
|
|
10850
|
+
}
|
|
10851
|
+
const startupSlots = parsePositiveInteger(domain.startupSlots, `${label}.startupSlots`);
|
|
10852
|
+
const heavySlots = parsePositiveInteger(domain.heavySlots, `${label}.heavySlots`);
|
|
10853
|
+
if (adapter === "macos-host-v1") {
|
|
10854
|
+
if (Object.hasOwn(domain, "unmanagedAllowanceBytes")) {
|
|
10855
|
+
throw new Error(
|
|
10856
|
+
`${label}.unmanagedAllowanceBytes is only supported for adapter 'macos-declared-v1'.`
|
|
10857
|
+
);
|
|
10858
|
+
}
|
|
10859
|
+
return {
|
|
10860
|
+
kind: "host",
|
|
10861
|
+
adapter: "macos-host-v1",
|
|
10862
|
+
capacityBytes,
|
|
10863
|
+
protectedHeadroomBytes,
|
|
10864
|
+
startupSlots,
|
|
10865
|
+
heavySlots
|
|
10866
|
+
};
|
|
10867
|
+
}
|
|
10868
|
+
const unmanagedAllowanceBytes = parseSafeInteger(
|
|
10869
|
+
domain.unmanagedAllowanceBytes,
|
|
10870
|
+
`${label}.unmanagedAllowanceBytes`,
|
|
10871
|
+
0
|
|
10872
|
+
);
|
|
10873
|
+
if (unmanagedAllowanceBytes > capacityBytes - protectedHeadroomBytes) {
|
|
10874
|
+
throw new Error(
|
|
10875
|
+
`${label}.unmanagedAllowanceBytes must not exceed capacityBytes minus protectedHeadroomBytes.`
|
|
10876
|
+
);
|
|
10877
|
+
}
|
|
10878
|
+
return {
|
|
10879
|
+
kind: "host",
|
|
10880
|
+
adapter: "macos-declared-v1",
|
|
10881
|
+
capacityBytes,
|
|
10882
|
+
protectedHeadroomBytes,
|
|
10883
|
+
unmanagedAllowanceBytes,
|
|
10884
|
+
startupSlots,
|
|
10885
|
+
heavySlots
|
|
10886
|
+
};
|
|
10887
|
+
}
|
|
10888
|
+
if (kind === "runtime") {
|
|
10889
|
+
ensureAllowedKeys2(
|
|
10890
|
+
domain,
|
|
10891
|
+
[
|
|
10892
|
+
"kind",
|
|
10893
|
+
"adapter",
|
|
10894
|
+
"endpoint",
|
|
10895
|
+
"daemonId",
|
|
10896
|
+
"hostDomain",
|
|
10897
|
+
"hostChargeCeilingBytes",
|
|
10898
|
+
"capacityBytes",
|
|
10899
|
+
"protectedHeadroomBytes",
|
|
10900
|
+
"guestUnmanagedAllowanceBytes",
|
|
10901
|
+
"startupSlots",
|
|
10902
|
+
"heavySlots"
|
|
10903
|
+
],
|
|
10904
|
+
label
|
|
10905
|
+
);
|
|
10906
|
+
const adapter = parseBoundedString(domain.adapter, `${label}.adapter`);
|
|
10907
|
+
if (adapter !== "orbstack-local-v1" && adapter !== "orbstack-declared-v1") {
|
|
10908
|
+
throw new Error(`${label}.adapter must be 'orbstack-local-v1' or 'orbstack-declared-v1'.`);
|
|
10909
|
+
}
|
|
10910
|
+
const endpoint = parseCanonicalAbsolutePath(domain.endpoint, `${label}.endpoint`);
|
|
10911
|
+
const daemonId = parseBoundedString(domain.daemonId, `${label}.daemonId`);
|
|
10912
|
+
const hostDomain = parseDomainId(domain.hostDomain, `${label}.hostDomain`);
|
|
10913
|
+
const hostChargeCeilingBytes = parsePositiveInteger(
|
|
10914
|
+
domain.hostChargeCeilingBytes,
|
|
10915
|
+
`${label}.hostChargeCeilingBytes`
|
|
10916
|
+
);
|
|
10917
|
+
const capacityBytes = parsePositiveInteger(domain.capacityBytes, `${label}.capacityBytes`);
|
|
10918
|
+
const protectedHeadroomBytes = parsePositiveInteger(
|
|
10919
|
+
domain.protectedHeadroomBytes,
|
|
10920
|
+
`${label}.protectedHeadroomBytes`
|
|
10921
|
+
);
|
|
10922
|
+
if (protectedHeadroomBytes >= capacityBytes) {
|
|
10923
|
+
throw new Error(`${label}.protectedHeadroomBytes must be less than capacityBytes.`);
|
|
10924
|
+
}
|
|
10925
|
+
const startupSlots = parsePositiveInteger(domain.startupSlots, `${label}.startupSlots`);
|
|
10926
|
+
const heavySlots = parsePositiveInteger(domain.heavySlots, `${label}.heavySlots`);
|
|
10927
|
+
if (adapter === "orbstack-local-v1") {
|
|
10928
|
+
if (Object.hasOwn(domain, "guestUnmanagedAllowanceBytes")) {
|
|
10929
|
+
throw new Error(
|
|
10930
|
+
`${label}.guestUnmanagedAllowanceBytes is only supported for adapter 'orbstack-declared-v1'.`
|
|
10931
|
+
);
|
|
10932
|
+
}
|
|
10933
|
+
return {
|
|
10934
|
+
kind: "runtime",
|
|
10935
|
+
adapter: "orbstack-local-v1",
|
|
10936
|
+
endpoint,
|
|
10937
|
+
daemonId,
|
|
10938
|
+
hostDomain,
|
|
10939
|
+
hostChargeCeilingBytes,
|
|
10940
|
+
capacityBytes,
|
|
10941
|
+
protectedHeadroomBytes,
|
|
10942
|
+
startupSlots,
|
|
10943
|
+
heavySlots
|
|
10944
|
+
};
|
|
10945
|
+
}
|
|
10946
|
+
return {
|
|
10947
|
+
kind: "runtime",
|
|
10948
|
+
adapter: "orbstack-declared-v1",
|
|
10949
|
+
endpoint,
|
|
10950
|
+
daemonId,
|
|
10951
|
+
hostDomain,
|
|
10952
|
+
hostChargeCeilingBytes,
|
|
10953
|
+
capacityBytes,
|
|
10954
|
+
protectedHeadroomBytes,
|
|
10955
|
+
guestUnmanagedAllowanceBytes: parseSafeInteger(
|
|
10956
|
+
domain.guestUnmanagedAllowanceBytes,
|
|
10957
|
+
`${label}.guestUnmanagedAllowanceBytes`,
|
|
10958
|
+
0
|
|
10959
|
+
),
|
|
10960
|
+
startupSlots,
|
|
10961
|
+
heavySlots
|
|
10962
|
+
};
|
|
10963
|
+
}
|
|
10964
|
+
throw new Error(`${label}.kind must be 'host' or 'runtime'.`);
|
|
10965
|
+
}
|
|
10966
|
+
function parseScheduling(value) {
|
|
10967
|
+
const label = "scheduling";
|
|
10968
|
+
const scheduling = ensureObject2(value, label);
|
|
10969
|
+
ensureAllowedKeys2(
|
|
10970
|
+
scheduling,
|
|
10971
|
+
[
|
|
10972
|
+
"maxQueuedPerDomain",
|
|
10973
|
+
"maxQueuedTotal",
|
|
10974
|
+
"queueLifetimeSeconds",
|
|
10975
|
+
"clientWaitSeconds",
|
|
10976
|
+
"maxClientWaitSeconds",
|
|
10977
|
+
"watchSeconds",
|
|
10978
|
+
"sampleIntervalSeconds",
|
|
10979
|
+
"maxSampleAgeSeconds"
|
|
10980
|
+
],
|
|
10981
|
+
label
|
|
10982
|
+
);
|
|
10983
|
+
const result = {
|
|
10984
|
+
maxQueuedPerDomain: parsePositiveInteger(
|
|
10985
|
+
scheduling.maxQueuedPerDomain,
|
|
10986
|
+
`${label}.maxQueuedPerDomain`
|
|
10987
|
+
),
|
|
10988
|
+
maxQueuedTotal: parsePositiveInteger(scheduling.maxQueuedTotal, `${label}.maxQueuedTotal`),
|
|
10989
|
+
queueLifetimeSeconds: parsePositiveInteger(
|
|
10990
|
+
scheduling.queueLifetimeSeconds,
|
|
10991
|
+
`${label}.queueLifetimeSeconds`
|
|
10992
|
+
),
|
|
10993
|
+
// Reserved for future runtime binding: validated and bounded today, but no
|
|
10994
|
+
// consumer reads these scheduling knobs yet. Runtime constants remain
|
|
10995
|
+
// authoritative (controller watch window, CLI admission wait, tick cadence).
|
|
10996
|
+
clientWaitSeconds: parseSafeInteger(
|
|
10997
|
+
scheduling.clientWaitSeconds,
|
|
10998
|
+
`${label}.clientWaitSeconds`,
|
|
10999
|
+
0
|
|
11000
|
+
),
|
|
11001
|
+
maxClientWaitSeconds: parsePositiveInteger(
|
|
11002
|
+
scheduling.maxClientWaitSeconds,
|
|
11003
|
+
`${label}.maxClientWaitSeconds`
|
|
11004
|
+
),
|
|
11005
|
+
watchSeconds: parsePositiveInteger(scheduling.watchSeconds, `${label}.watchSeconds`),
|
|
11006
|
+
sampleIntervalSeconds: parsePositiveInteger(
|
|
11007
|
+
scheduling.sampleIntervalSeconds,
|
|
11008
|
+
`${label}.sampleIntervalSeconds`
|
|
11009
|
+
),
|
|
11010
|
+
maxSampleAgeSeconds: parsePositiveInteger(
|
|
11011
|
+
scheduling.maxSampleAgeSeconds,
|
|
11012
|
+
`${label}.maxSampleAgeSeconds`
|
|
11013
|
+
)
|
|
11014
|
+
};
|
|
11015
|
+
if (result.clientWaitSeconds > result.maxClientWaitSeconds) {
|
|
11016
|
+
throw new Error(`${label}.clientWaitSeconds must not exceed maxClientWaitSeconds.`);
|
|
11017
|
+
}
|
|
11018
|
+
if (result.maxQueuedTotal > 64 || result.maxQueuedPerDomain > 32 || result.queueLifetimeSeconds > 900 || result.maxClientWaitSeconds > 900 || result.watchSeconds > 30 || result.maxSampleAgeSeconds > 15) {
|
|
11019
|
+
throw new Error(`${label} exceeds supported controller bounds.`);
|
|
11020
|
+
}
|
|
11021
|
+
if (result.queueLifetimeSeconds < result.maxClientWaitSeconds) {
|
|
11022
|
+
throw new Error(`${label}.queueLifetimeSeconds must cover maxClientWaitSeconds.`);
|
|
11023
|
+
}
|
|
11024
|
+
if (result.maxSampleAgeSeconds < result.sampleIntervalSeconds) {
|
|
11025
|
+
throw new Error(`${label}.maxSampleAgeSeconds must be at least sampleIntervalSeconds.`);
|
|
11026
|
+
}
|
|
11027
|
+
return result;
|
|
11028
|
+
}
|
|
11029
|
+
function parseRecovery(value) {
|
|
11030
|
+
if (value === void 0) return { ...DEFAULT_CAPACITY_RECOVERY };
|
|
11031
|
+
const label = "recovery";
|
|
11032
|
+
const recovery = ensureObject2(value, label);
|
|
11033
|
+
ensureAllowedKeys2(
|
|
11034
|
+
recovery,
|
|
11035
|
+
[
|
|
11036
|
+
"enabled",
|
|
11037
|
+
"maxProcessRestarts",
|
|
11038
|
+
"maxServiceRestarts",
|
|
11039
|
+
"maxCorrectiveActions",
|
|
11040
|
+
"windowSeconds",
|
|
11041
|
+
"observationSeconds",
|
|
11042
|
+
"resumeDwellSeconds"
|
|
11043
|
+
],
|
|
11044
|
+
label
|
|
11045
|
+
);
|
|
11046
|
+
if (typeof recovery.enabled !== "boolean") {
|
|
11047
|
+
throw new Error(`${label}.enabled must be a boolean.`);
|
|
11048
|
+
}
|
|
11049
|
+
const result = {
|
|
11050
|
+
enabled: recovery.enabled,
|
|
11051
|
+
maxProcessRestarts: parseSafeInteger(
|
|
11052
|
+
recovery.maxProcessRestarts,
|
|
11053
|
+
`${label}.maxProcessRestarts`,
|
|
11054
|
+
0
|
|
11055
|
+
),
|
|
11056
|
+
maxServiceRestarts: parseSafeInteger(
|
|
11057
|
+
recovery.maxServiceRestarts,
|
|
11058
|
+
`${label}.maxServiceRestarts`,
|
|
11059
|
+
0
|
|
11060
|
+
),
|
|
11061
|
+
maxCorrectiveActions: parsePositiveInteger(
|
|
11062
|
+
recovery.maxCorrectiveActions,
|
|
11063
|
+
`${label}.maxCorrectiveActions`
|
|
11064
|
+
),
|
|
11065
|
+
windowSeconds: parsePositiveInteger(recovery.windowSeconds, `${label}.windowSeconds`),
|
|
11066
|
+
observationSeconds: parsePositiveInteger(
|
|
11067
|
+
recovery.observationSeconds,
|
|
11068
|
+
`${label}.observationSeconds`
|
|
11069
|
+
),
|
|
11070
|
+
resumeDwellSeconds: parsePositiveInteger(
|
|
11071
|
+
recovery.resumeDwellSeconds,
|
|
11072
|
+
`${label}.resumeDwellSeconds`
|
|
11073
|
+
)
|
|
11074
|
+
};
|
|
11075
|
+
if (result.maxCorrectiveActions < result.maxProcessRestarts + result.maxServiceRestarts) {
|
|
11076
|
+
throw new Error(
|
|
11077
|
+
`${label}.maxCorrectiveActions must cover maxProcessRestarts and maxServiceRestarts.`
|
|
11078
|
+
);
|
|
11079
|
+
}
|
|
11080
|
+
if (result.maxProcessRestarts > 4 || result.maxServiceRestarts > 2 || result.maxCorrectiveActions > 8 || result.windowSeconds > 3600 || result.observationSeconds > 300 || result.resumeDwellSeconds > 3600) {
|
|
11081
|
+
throw new Error(`${label} exceeds supported controller bounds.`);
|
|
11082
|
+
}
|
|
11083
|
+
return result;
|
|
11084
|
+
}
|
|
11085
|
+
function validateDomainReferences(domains) {
|
|
11086
|
+
const daemonIds = /* @__PURE__ */ new Map();
|
|
11087
|
+
const endpoints = /* @__PURE__ */ new Map();
|
|
11088
|
+
for (const [domainId, domain] of Object.entries(domains)) {
|
|
11089
|
+
if (domain.kind !== "runtime") continue;
|
|
11090
|
+
const previousDaemon = daemonIds.get(domain.daemonId);
|
|
11091
|
+
if (previousDaemon) {
|
|
11092
|
+
throw new Error(
|
|
11093
|
+
`domains.${domainId}.daemonId duplicates runtime domain '${previousDaemon}'.`
|
|
11094
|
+
);
|
|
11095
|
+
}
|
|
11096
|
+
daemonIds.set(domain.daemonId, domainId);
|
|
11097
|
+
const previousEndpoint = endpoints.get(domain.endpoint);
|
|
11098
|
+
if (previousEndpoint) {
|
|
11099
|
+
throw new Error(`domains.${domainId}.endpoint aliases runtime domain '${previousEndpoint}'.`);
|
|
11100
|
+
}
|
|
11101
|
+
endpoints.set(domain.endpoint, domainId);
|
|
11102
|
+
if (!Object.hasOwn(domains, domain.hostDomain)) {
|
|
11103
|
+
throw new Error(
|
|
11104
|
+
`domains.${domainId}.hostDomain references missing domain '${domain.hostDomain}'.`
|
|
11105
|
+
);
|
|
11106
|
+
}
|
|
11107
|
+
const hostDomain = domains[domain.hostDomain];
|
|
11108
|
+
if (hostDomain.kind !== "host") {
|
|
11109
|
+
throw new Error(
|
|
11110
|
+
`domains.${domainId}.hostDomain must reference a host domain, not '${domain.hostDomain}'.`
|
|
11111
|
+
);
|
|
11112
|
+
}
|
|
11113
|
+
const unmanagedAllowanceBytes = hostDomain.adapter === "macos-declared-v1" ? hostDomain.unmanagedAllowanceBytes : 0;
|
|
11114
|
+
const admissibleHostBytes = hostDomain.capacityBytes - hostDomain.protectedHeadroomBytes - unmanagedAllowanceBytes;
|
|
11115
|
+
if (domain.hostChargeCeilingBytes > admissibleHostBytes) {
|
|
11116
|
+
throw new Error(
|
|
11117
|
+
`domains.${domainId}.hostChargeCeilingBytes exceeds admissible capacity of host domain '${domain.hostDomain}'.`
|
|
11118
|
+
);
|
|
11119
|
+
}
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
function parseEnrollment(value, index, domains) {
|
|
11123
|
+
const label = `enrollments[${index}]`;
|
|
11124
|
+
const enrollment = ensureObject2(value, label);
|
|
11125
|
+
ensureAllowedKeys2(
|
|
11126
|
+
enrollment,
|
|
11127
|
+
[
|
|
11128
|
+
"repoPath",
|
|
11129
|
+
"gitCommonDir",
|
|
11130
|
+
"workspace",
|
|
11131
|
+
"provider",
|
|
11132
|
+
"providerId",
|
|
11133
|
+
"hostDomain",
|
|
11134
|
+
"runtimeDomain",
|
|
11135
|
+
"profiles",
|
|
11136
|
+
"estimatesDigest",
|
|
11137
|
+
"defaultOperation"
|
|
11138
|
+
],
|
|
11139
|
+
label
|
|
11140
|
+
);
|
|
11141
|
+
const repoPath = parseCanonicalAbsolutePath(enrollment.repoPath, `${label}.repoPath`);
|
|
11142
|
+
const gitCommonDir = parseCanonicalAbsolutePath(enrollment.gitCommonDir, `${label}.gitCommonDir`);
|
|
11143
|
+
const workspace = parseBoundedString(enrollment.workspace, `${label}.workspace`, 32);
|
|
11144
|
+
if (!WORKSPACE_TOKEN_RE.test(workspace)) {
|
|
11145
|
+
throw new Error(`${label}.workspace must be an existing workspace token.`);
|
|
11146
|
+
}
|
|
11147
|
+
const provider = parseBoundedString(enrollment.provider, `${label}.provider`);
|
|
11148
|
+
if (provider !== "devsy" && provider !== "devpod") {
|
|
11149
|
+
throw new Error(`${label}.provider must be 'devsy' or 'devpod'.`);
|
|
11150
|
+
}
|
|
11151
|
+
const providerId = parseBoundedString(enrollment.providerId, `${label}.providerId`);
|
|
11152
|
+
const hostDomain = parseDomainId(enrollment.hostDomain, `${label}.hostDomain`);
|
|
11153
|
+
const runtimeDomain = parseDomainId(enrollment.runtimeDomain, `${label}.runtimeDomain`);
|
|
11154
|
+
if (!Object.hasOwn(domains, hostDomain)) {
|
|
11155
|
+
throw new Error(`${label}.hostDomain references missing domain '${hostDomain}'.`);
|
|
11156
|
+
}
|
|
11157
|
+
const host = domains[hostDomain];
|
|
11158
|
+
if (host.kind !== "host") {
|
|
11159
|
+
throw new Error(`${label}.hostDomain must reference a host domain.`);
|
|
11160
|
+
}
|
|
11161
|
+
if (!Object.hasOwn(domains, runtimeDomain)) {
|
|
11162
|
+
throw new Error(`${label}.runtimeDomain references missing domain '${runtimeDomain}'.`);
|
|
11163
|
+
}
|
|
11164
|
+
const runtime = domains[runtimeDomain];
|
|
11165
|
+
if (runtime.kind !== "runtime") {
|
|
11166
|
+
throw new Error(`${label}.runtimeDomain must reference a runtime domain.`);
|
|
11167
|
+
}
|
|
11168
|
+
if (runtime.hostDomain !== hostDomain) {
|
|
11169
|
+
throw new Error(
|
|
11170
|
+
`${label}.runtimeDomain '${runtimeDomain}' is bound to host domain '${runtime.hostDomain}', not '${hostDomain}'.`
|
|
11171
|
+
);
|
|
11172
|
+
}
|
|
11173
|
+
const profiles = parseUniqueProfiles(enrollment.profiles, `${label}.profiles`);
|
|
11174
|
+
const estimatesDigest = parseDigest(enrollment.estimatesDigest, `${label}.estimatesDigest`);
|
|
11175
|
+
const defaultOperation = ensureObject2(enrollment.defaultOperation, `${label}.defaultOperation`);
|
|
11176
|
+
ensureAllowedKeys2(
|
|
11177
|
+
defaultOperation,
|
|
11178
|
+
["hostIncrementBytes", "runtimeIncrementBytes"],
|
|
11179
|
+
`${label}.defaultOperation`
|
|
11180
|
+
);
|
|
11181
|
+
return {
|
|
11182
|
+
repoPath,
|
|
11183
|
+
gitCommonDir,
|
|
11184
|
+
workspace,
|
|
11185
|
+
provider,
|
|
11186
|
+
providerId,
|
|
11187
|
+
hostDomain,
|
|
11188
|
+
runtimeDomain,
|
|
11189
|
+
profiles,
|
|
11190
|
+
estimatesDigest,
|
|
11191
|
+
defaultOperation: {
|
|
11192
|
+
hostIncrementBytes: parseSafeInteger(
|
|
11193
|
+
defaultOperation.hostIncrementBytes,
|
|
11194
|
+
`${label}.defaultOperation.hostIncrementBytes`,
|
|
11195
|
+
0
|
|
11196
|
+
),
|
|
11197
|
+
runtimeIncrementBytes: parsePositiveInteger(
|
|
11198
|
+
defaultOperation.runtimeIncrementBytes,
|
|
11199
|
+
`${label}.defaultOperation.runtimeIncrementBytes`
|
|
11200
|
+
)
|
|
11201
|
+
}
|
|
11202
|
+
};
|
|
11203
|
+
}
|
|
11204
|
+
function parseCapacityPolicy(value) {
|
|
11205
|
+
const root = ensureObject2(value, "capacity policy");
|
|
11206
|
+
ensureAllowedKeys2(
|
|
11207
|
+
root,
|
|
11208
|
+
["version", "revision", "admissions", "scheduling", "recovery", "domains", "enrollments"],
|
|
11209
|
+
"capacity policy"
|
|
11210
|
+
);
|
|
11211
|
+
const version = parsePositiveInteger(root.version, "capacity policy.version");
|
|
11212
|
+
if (version !== 1) {
|
|
11213
|
+
throw new Error("capacity policy.version must be 1.");
|
|
11214
|
+
}
|
|
11215
|
+
const revision = parsePositiveInteger(root.revision, "capacity policy.revision");
|
|
11216
|
+
const admissions = parseBoundedString(root.admissions, "capacity policy.admissions");
|
|
11217
|
+
if (admissions !== "enabled" && admissions !== "paused") {
|
|
11218
|
+
throw new Error("capacity policy.admissions must be 'enabled' or 'paused'.");
|
|
11219
|
+
}
|
|
11220
|
+
const scheduling = parseScheduling(root.scheduling);
|
|
11221
|
+
const recovery = parseRecovery(root.recovery);
|
|
11222
|
+
const domainsValue = ensureObject2(root.domains, "capacity policy.domains");
|
|
11223
|
+
const domainEntries = Object.entries(domainsValue);
|
|
11224
|
+
if (domainEntries.length > MAX_DOMAINS) {
|
|
11225
|
+
throw new Error(`capacity policy.domains exceeds the maximum of ${MAX_DOMAINS} domains.`);
|
|
11226
|
+
}
|
|
11227
|
+
const domains = {};
|
|
11228
|
+
for (const [domainId, domainValue] of domainEntries) {
|
|
11229
|
+
const parsedId = parseDomainId(domainId, "capacity policy.domains key");
|
|
11230
|
+
domains[parsedId] = parseDomain(domainValue, parsedId);
|
|
11231
|
+
}
|
|
11232
|
+
validateDomainReferences(domains);
|
|
11233
|
+
if (!Array.isArray(root.enrollments)) {
|
|
11234
|
+
throw new Error("capacity policy.enrollments must be an array.");
|
|
11235
|
+
}
|
|
11236
|
+
if (root.enrollments.length > MAX_ENROLLMENTS) {
|
|
11237
|
+
throw new Error(
|
|
11238
|
+
`capacity policy.enrollments exceeds the maximum of ${MAX_ENROLLMENTS} enrollments.`
|
|
11239
|
+
);
|
|
11240
|
+
}
|
|
11241
|
+
const enrollments = [];
|
|
11242
|
+
const enrollmentKeys = /* @__PURE__ */ new Set();
|
|
11243
|
+
const providerIds = /* @__PURE__ */ new Map();
|
|
11244
|
+
for (const [index, enrollmentValue] of root.enrollments.entries()) {
|
|
11245
|
+
const enrollment = parseEnrollment(enrollmentValue, index, domains);
|
|
11246
|
+
const enrollmentKey = [enrollment.repoPath, enrollment.gitCommonDir, enrollment.workspace].join(
|
|
11247
|
+
"\0"
|
|
11248
|
+
);
|
|
11249
|
+
if (enrollmentKeys.has(enrollmentKey)) {
|
|
11250
|
+
throw new Error(`enrollments[${index}] duplicates an existing enrollment.`);
|
|
11251
|
+
}
|
|
11252
|
+
enrollmentKeys.add(enrollmentKey);
|
|
11253
|
+
const providerKey = `${enrollment.provider}\0${enrollment.providerId}`;
|
|
11254
|
+
const previousProviderIndex = providerIds.get(providerKey);
|
|
11255
|
+
if (previousProviderIndex !== void 0) {
|
|
11256
|
+
throw new Error(
|
|
11257
|
+
`enrollments[${index}].providerId duplicates enrollments[${previousProviderIndex}].`
|
|
11258
|
+
);
|
|
11259
|
+
}
|
|
11260
|
+
providerIds.set(providerKey, index);
|
|
11261
|
+
enrollments.push(enrollment);
|
|
11262
|
+
}
|
|
11263
|
+
return {
|
|
11264
|
+
version: 1,
|
|
11265
|
+
revision,
|
|
11266
|
+
admissions,
|
|
11267
|
+
scheduling,
|
|
11268
|
+
recovery,
|
|
11269
|
+
domains,
|
|
11270
|
+
enrollments
|
|
11271
|
+
};
|
|
11272
|
+
}
|
|
11273
|
+
|
|
11274
|
+
// src/core/controller-client.ts
|
|
11275
|
+
var import_node_crypto15 = require("crypto");
|
|
11276
|
+
var import_node_fs28 = __toESM(require("fs"));
|
|
11277
|
+
var import_node_net2 = __toESM(require("net"));
|
|
11278
|
+
var import_node_path33 = __toESM(require("path"));
|
|
11279
|
+
|
|
11280
|
+
// src/core/controller-protocol.ts
|
|
11281
|
+
var import_node_path32 = __toESM(require("path"));
|
|
11282
|
+
var CONTROLLER_FRAME_BYTES = 64 * 1024;
|
|
11283
|
+
var CONTROLLER_MAX_OPERATION_REQUEST_BYTES = 32 * 1024;
|
|
11284
|
+
var CONTROLLER_PROFILE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11285
|
+
var CONTROLLER_PROFILE_RE = new RegExp(
|
|
11286
|
+
`^(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)})(?:,(?:${CONTROLLER_PROFILE_TOKEN_RE.source.slice(1, -1)}))*$`
|
|
11287
|
+
);
|
|
11288
|
+
|
|
11289
|
+
// src/core/controller-monitor.ts
|
|
11290
|
+
var import_node_crypto16 = require("crypto");
|
|
11291
|
+
var import_node_util6 = require("util");
|
|
11292
|
+
|
|
11293
|
+
// src/core/reliability-output.ts
|
|
11294
|
+
function reliabilityAttention(state) {
|
|
11295
|
+
assertReliabilityState(state);
|
|
11296
|
+
const completeStop = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
|
|
11297
|
+
if (state.desired === "stopped-by-user") return completeStop ? void 0 : "stop-incomplete";
|
|
11298
|
+
if (state.operation?.status === "COMPLETION_UNKNOWN" || state.operation?.status === "INTERRUPTED")
|
|
11299
|
+
return "operation-unknown";
|
|
11300
|
+
if (state.desired === "parked-for-capacity")
|
|
11301
|
+
return completeStop ? "capacity-parked" : "stop-incomplete";
|
|
11302
|
+
if (state.admission === "denied-unadmittable") return "unadmittable";
|
|
11303
|
+
if (state.admission === "waiting") return "capacity-waiting";
|
|
11304
|
+
if (state.phase === "recovering") return "recovering";
|
|
11305
|
+
if (state.phase === "queued" || state.phase === "starting" || state.phase === "verifying")
|
|
11306
|
+
return "starting";
|
|
11307
|
+
return void 0;
|
|
11308
|
+
}
|
|
11309
|
+
|
|
11310
|
+
// src/core/managed-post-start.ts
|
|
11311
|
+
var import_node_child_process20 = require("child_process");
|
|
11312
|
+
var import_node_crypto18 = require("crypto");
|
|
11313
|
+
var import_node_fs29 = __toESM(require("fs"));
|
|
11314
|
+
var import_node_path34 = __toESM(require("path"));
|
|
11315
|
+
|
|
11316
|
+
// src/core/managed-host-ports.ts
|
|
11317
|
+
var import_node_child_process19 = require("child_process");
|
|
11318
|
+
var import_node_crypto17 = require("crypto");
|
|
11319
|
+
var PORT_LOOKUP_TIMEOUT_MS = 1e4;
|
|
11320
|
+
var PORT_LOOKUP_MAX_BUFFER = 64 * 1024;
|
|
11321
|
+
var LOOPBACK_BINDING = /^127\.0\.0\.1:(\d+)$/;
|
|
11322
|
+
function unavailableEntry(declaration, reason2) {
|
|
11323
|
+
return { ...declaration, status: "unavailable", reason: reason2 };
|
|
11324
|
+
}
|
|
11325
|
+
function livePublishedBindings(containerId, target) {
|
|
11326
|
+
const result = (0, import_node_child_process19.spawnSync)("docker", ["port", containerId, `${target}/tcp`], {
|
|
11327
|
+
encoding: "utf-8",
|
|
11328
|
+
timeout: PORT_LOOKUP_TIMEOUT_MS,
|
|
11329
|
+
maxBuffer: PORT_LOOKUP_MAX_BUFFER,
|
|
11330
|
+
...networkDockerOptions()
|
|
11331
|
+
});
|
|
11332
|
+
if (result.error || result.status === null) return void 0;
|
|
11333
|
+
if (result.status !== 0) return "";
|
|
11334
|
+
return typeof result.stdout === "string" ? result.stdout : void 0;
|
|
11335
|
+
}
|
|
11336
|
+
function parseLoopbackBinding(output) {
|
|
11337
|
+
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
11338
|
+
if (lines.length === 0) return { reason: "not-published" };
|
|
11339
|
+
if (lines.length > 1) return { reason: "ambiguous" };
|
|
11340
|
+
const match = LOOPBACK_BINDING.exec(lines[0]);
|
|
11341
|
+
if (!match) return { reason: "not-published" };
|
|
11342
|
+
const port = Number(match[1]);
|
|
11343
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return { reason: "not-published" };
|
|
11344
|
+
return { endpoint: { host: "127.0.0.1", port } };
|
|
11345
|
+
}
|
|
11346
|
+
function revalidateLiveOwner(containerId, input, composeProject, composeFiles, service) {
|
|
11347
|
+
try {
|
|
11348
|
+
const current = inspectWorkspaceContainers({ ids: [containerId] });
|
|
11349
|
+
if (current.length !== 1 || current[0].id !== containerId) return "unavailable";
|
|
11350
|
+
if (!hasExactComposeIdentity(current[0], {
|
|
11351
|
+
repoPath: input.repoPath,
|
|
11352
|
+
service,
|
|
11353
|
+
composeProject,
|
|
11354
|
+
composeFiles
|
|
11355
|
+
})) {
|
|
11356
|
+
return "unavailable";
|
|
11357
|
+
}
|
|
11358
|
+
return current[0].state.Running ? "available" : "not-running";
|
|
11359
|
+
} catch {
|
|
11360
|
+
return "unavailable";
|
|
11361
|
+
}
|
|
11362
|
+
}
|
|
11363
|
+
function resolveManagedHostPorts(input) {
|
|
11364
|
+
const report = {};
|
|
11365
|
+
const selectedServices = new Set(input.selectedServices);
|
|
11366
|
+
const selectedKeys = [];
|
|
11367
|
+
for (const [key, declaration] of Object.entries(input.declarations)) {
|
|
11368
|
+
if (!selectedServices.has(declaration.service)) {
|
|
11369
|
+
report[key] = { ...declaration, status: "inactive" };
|
|
11370
|
+
continue;
|
|
11371
|
+
}
|
|
11372
|
+
selectedKeys.push(key);
|
|
11373
|
+
report[key] = { ...declaration, status: "unavailable", reason: "unavailable" };
|
|
11374
|
+
}
|
|
11375
|
+
if (selectedKeys.length === 0) return report;
|
|
11376
|
+
const composeProject = input.composeProject;
|
|
11377
|
+
const composeFiles = input.composeFiles;
|
|
11378
|
+
if (typeof composeProject !== "string" || composeProject.length === 0 || composeFiles === void 0 || composeFiles.length === 0 || composeFiles.some((file) => file.length === 0)) {
|
|
11379
|
+
return report;
|
|
11380
|
+
}
|
|
11381
|
+
if (selectedKeys.some((key) => {
|
|
11382
|
+
const target = input.declarations[key].target;
|
|
11383
|
+
return !Number.isInteger(target) || target < 1 || target > 65535;
|
|
11384
|
+
})) {
|
|
11385
|
+
return report;
|
|
11386
|
+
}
|
|
11387
|
+
const liveContainers = input.containers === void 0;
|
|
11388
|
+
let containers = input.containers;
|
|
11389
|
+
if (containers === void 0) {
|
|
11390
|
+
try {
|
|
11391
|
+
containers = inspectWorkspaceContainers({ composeProject });
|
|
11392
|
+
} catch {
|
|
11393
|
+
return report;
|
|
11394
|
+
}
|
|
11395
|
+
}
|
|
11396
|
+
const publishedBindings = input.publishedBindings ?? livePublishedBindings;
|
|
11397
|
+
for (const key of selectedKeys) {
|
|
11398
|
+
const declaration = input.declarations[key];
|
|
11399
|
+
let owners;
|
|
11400
|
+
try {
|
|
11401
|
+
owners = containers.filter(
|
|
11402
|
+
(container) => hasExactComposeIdentity(container, {
|
|
11403
|
+
repoPath: input.repoPath,
|
|
11404
|
+
service: declaration.service,
|
|
11405
|
+
composeProject,
|
|
11406
|
+
composeFiles
|
|
11407
|
+
})
|
|
11408
|
+
);
|
|
11409
|
+
} catch {
|
|
11410
|
+
report[key] = unavailableEntry(declaration, "unavailable");
|
|
11411
|
+
continue;
|
|
11412
|
+
}
|
|
11413
|
+
if (owners.length === 0) {
|
|
11414
|
+
report[key] = unavailableEntry(declaration, "not-found");
|
|
11415
|
+
continue;
|
|
11416
|
+
}
|
|
11417
|
+
if (owners.length > 1) {
|
|
11418
|
+
report[key] = unavailableEntry(declaration, "ambiguous");
|
|
11419
|
+
continue;
|
|
11420
|
+
}
|
|
11421
|
+
const owner = owners[0];
|
|
11422
|
+
if (!owner.state.Running) {
|
|
11423
|
+
report[key] = unavailableEntry(declaration, "not-running");
|
|
11424
|
+
continue;
|
|
11425
|
+
}
|
|
11426
|
+
let output;
|
|
11427
|
+
try {
|
|
11428
|
+
output = publishedBindings(owner.id, declaration.target);
|
|
11429
|
+
} catch {
|
|
11430
|
+
output = void 0;
|
|
11431
|
+
}
|
|
11432
|
+
if (typeof output !== "string") {
|
|
11433
|
+
report[key] = unavailableEntry(declaration, "unavailable");
|
|
11434
|
+
continue;
|
|
11435
|
+
}
|
|
11436
|
+
const binding = parseLoopbackBinding(output);
|
|
11437
|
+
if (!binding.endpoint) {
|
|
11438
|
+
report[key] = unavailableEntry(declaration, binding.reason ?? "unavailable");
|
|
11439
|
+
continue;
|
|
11440
|
+
}
|
|
11441
|
+
if (liveContainers) {
|
|
11442
|
+
const proof = revalidateLiveOwner(
|
|
11443
|
+
owner.id,
|
|
11444
|
+
input,
|
|
11445
|
+
composeProject,
|
|
11446
|
+
composeFiles,
|
|
11447
|
+
declaration.service
|
|
11448
|
+
);
|
|
11449
|
+
if (proof !== "available") {
|
|
11450
|
+
report[key] = unavailableEntry(declaration, proof);
|
|
11451
|
+
continue;
|
|
11452
|
+
}
|
|
11453
|
+
}
|
|
11454
|
+
report[key] = {
|
|
11455
|
+
...declaration,
|
|
11456
|
+
status: "available",
|
|
11457
|
+
endpoint: binding.endpoint
|
|
11458
|
+
};
|
|
11459
|
+
}
|
|
11460
|
+
return report;
|
|
11461
|
+
}
|
|
11462
|
+
function managedHostPortDeclarationsIdentity(declarations) {
|
|
11463
|
+
const keys2 = Object.keys(declarations).sort();
|
|
11464
|
+
if (keys2.length === 0) return void 0;
|
|
11465
|
+
const canonical = keys2.map((key) => [key, declarations[key].service, declarations[key].target]);
|
|
11466
|
+
return (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
11467
|
+
}
|
|
11468
|
+
function managedHostPortVariables(report) {
|
|
11469
|
+
return Object.fromEntries(
|
|
11470
|
+
Object.entries(report).map(([key, entry]) => [
|
|
11471
|
+
`DEVROUTER_HOST_PORT_${key.toUpperCase().replaceAll("-", "_")}`,
|
|
11472
|
+
entry.status === "available" && entry.endpoint ? String(entry.endpoint.port) : ""
|
|
11473
|
+
])
|
|
11474
|
+
);
|
|
11475
|
+
}
|
|
11476
|
+
function managedHostPortIdentity(report) {
|
|
11477
|
+
const keys2 = Object.keys(report).sort();
|
|
11478
|
+
if (keys2.length === 0) return void 0;
|
|
11479
|
+
const canonical = keys2.map((key) => {
|
|
11480
|
+
const entry = report[key];
|
|
11481
|
+
return [key, entry.status === "available" ? entry.endpoint?.port ?? null : entry.status];
|
|
11482
|
+
});
|
|
11483
|
+
return (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
11484
|
+
}
|
|
11485
|
+
function assertManagedHostPortsAvailable(report) {
|
|
11486
|
+
const unavailable = Object.entries(report).filter(([, entry]) => entry.status === "unavailable").map(([key]) => key).sort();
|
|
11487
|
+
if (unavailable.length > 0) {
|
|
11488
|
+
throw new Error(`Selected managed host ports are unavailable: ${unavailable.join(", ")}.`);
|
|
11489
|
+
}
|
|
11490
|
+
}
|
|
11491
|
+
|
|
11492
|
+
// src/core/managed-post-start.ts
|
|
11493
|
+
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
11494
|
+
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
11495
|
+
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
11496
|
+
var RUNTIME_VARIANT_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11497
|
+
var ADAPTER_WRAPPER = `adapter_snapshot="$1"
|
|
11498
|
+
shift
|
|
11499
|
+
readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256 DEVROUTER_RUNTIME_VARIANT DEVROUTER_PROCESS_HOST_PORTS_SHA256
|
|
11500
|
+
source "$adapter_snapshot"
|
|
11501
|
+
`;
|
|
11502
|
+
function renderDeliveryScript(targetPath) {
|
|
11503
|
+
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
11504
|
+
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
11505
|
+
}
|
|
11506
|
+
const temporaryPrefix = import_node_path34.default.posix.basename(targetPath);
|
|
11507
|
+
return `set -eu
|
|
11508
|
+
umask 077
|
|
11509
|
+
runtime_root=/tmp/devrouter
|
|
11510
|
+
runtime_bin="$runtime_root/bin"
|
|
11511
|
+
target="${targetPath}"
|
|
11512
|
+
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
11513
|
+
echo "Refusing symlinked devrouter runtime path." >&2
|
|
11514
|
+
exit 1
|
|
11515
|
+
fi
|
|
11516
|
+
mkdir -p "$runtime_bin"
|
|
11517
|
+
chmod 700 "$runtime_root" "$runtime_bin"
|
|
11518
|
+
temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
|
|
11519
|
+
trap 'rm -f "$temporary"' EXIT
|
|
11520
|
+
cat > "$temporary"
|
|
11521
|
+
chmod 700 "$temporary"
|
|
11522
|
+
mv -f "$temporary" "$target"
|
|
11523
|
+
trap - EXIT
|
|
11524
|
+
`;
|
|
11525
|
+
}
|
|
11526
|
+
function commandFailure2(result) {
|
|
11527
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
11528
|
+
}
|
|
11529
|
+
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
11530
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
11531
|
+
"docker",
|
|
11532
|
+
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
11533
|
+
{ input: contents, encoding: "utf-8", ...networkDockerOptions() }
|
|
11534
|
+
);
|
|
11535
|
+
if (result.status !== 0) {
|
|
11536
|
+
const details = commandFailure2(result);
|
|
11537
|
+
throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
|
|
11538
|
+
}
|
|
11539
|
+
}
|
|
11540
|
+
function readRegularFileBytes(filePath) {
|
|
11541
|
+
if (!import_node_fs29.default.existsSync(filePath) || !import_node_fs29.default.lstatSync(filePath).isFile()) return void 0;
|
|
11542
|
+
return import_node_fs29.default.readFileSync(filePath);
|
|
11543
|
+
}
|
|
11544
|
+
function readRegularFile(filePath) {
|
|
11545
|
+
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
11546
|
+
}
|
|
11547
|
+
function adapterFingerprint(adapter) {
|
|
11548
|
+
return (0, import_node_crypto18.createHash)("sha256").update(adapter).digest("hex");
|
|
11549
|
+
}
|
|
11550
|
+
function resolveProcessHelperPath() {
|
|
11551
|
+
const candidates = [
|
|
11552
|
+
import_node_path34.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
11553
|
+
import_node_path34.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
11554
|
+
];
|
|
11555
|
+
const helperPath = candidates.find((candidate) => import_node_fs29.default.existsSync(candidate));
|
|
11556
|
+
if (!helperPath) {
|
|
11557
|
+
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
11558
|
+
}
|
|
11559
|
+
return helperPath;
|
|
11560
|
+
}
|
|
11561
|
+
function resolveManagedPostStartPlan(repoPath) {
|
|
11562
|
+
const adapterPath = import_node_path34.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
11563
|
+
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
11564
|
+
const adapterText = adapterBytes?.toString("utf-8");
|
|
11565
|
+
const dockerfilePath = import_node_path34.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
11566
|
+
const devcontainerPath = import_node_path34.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
11567
|
+
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
11568
|
+
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
11569
|
+
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
11570
|
+
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
11571
|
+
);
|
|
11572
|
+
if (adapterBytes === void 0) {
|
|
11573
|
+
if (devrouterPattern) {
|
|
11574
|
+
throw new Error(
|
|
11575
|
+
"Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
|
|
11576
|
+
);
|
|
11577
|
+
}
|
|
11578
|
+
return { kind: "unmanaged" };
|
|
11579
|
+
}
|
|
11580
|
+
const adapter = adapterBytes.toString("utf-8");
|
|
11581
|
+
if (!adapter.includes(MANAGED_MARKER)) {
|
|
11582
|
+
if (devrouterPattern) {
|
|
11583
|
+
throw new Error(
|
|
11584
|
+
"Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
11585
|
+
);
|
|
11586
|
+
}
|
|
11587
|
+
return { kind: "unmanaged" };
|
|
11588
|
+
}
|
|
11589
|
+
assertManagedDevcontainerLifecycle(repoPath);
|
|
11590
|
+
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
11591
|
+
return {
|
|
11592
|
+
kind: "runtime",
|
|
11593
|
+
adapterPath: MANAGED_ADAPTER_PATH,
|
|
11594
|
+
adapterSha256: adapterFingerprint(adapterBytes),
|
|
11595
|
+
adapterContents: adapterBytes
|
|
11596
|
+
};
|
|
11597
|
+
}
|
|
11598
|
+
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
11599
|
+
return { kind: "legacy" };
|
|
11600
|
+
}
|
|
11601
|
+
throw new Error(
|
|
11602
|
+
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
11603
|
+
);
|
|
11604
|
+
}
|
|
11605
|
+
function runManagedPostStart(options) {
|
|
11606
|
+
if (options.plan.kind !== "runtime") return;
|
|
11607
|
+
const runtimeVariant = options.variant ?? "default";
|
|
11608
|
+
if (runtimeVariant.length > 64 || !RUNTIME_VARIANT_PATTERN.test(runtimeVariant)) {
|
|
11609
|
+
throw new Error(
|
|
11610
|
+
"Managed runtime variant must use lowercase alphanumeric segments separated by hyphens and be at most 64 characters."
|
|
11611
|
+
);
|
|
11612
|
+
}
|
|
11613
|
+
const hostPorts = options.hostPorts ?? {};
|
|
11614
|
+
assertManagedHostPortsAvailable(hostPorts);
|
|
11615
|
+
const portEnvironment = managedHostPortVariables(hostPorts);
|
|
11616
|
+
const portIdentity = managedHostPortIdentity(hostPorts) ?? "";
|
|
11617
|
+
const helper = import_node_fs29.default.readFileSync(resolveProcessHelperPath());
|
|
11618
|
+
deliverRuntimeFile(
|
|
11619
|
+
options.container.id,
|
|
11620
|
+
RUNTIME_HELPER_PATH,
|
|
11621
|
+
helper,
|
|
11622
|
+
"the managed process helper"
|
|
11623
|
+
);
|
|
11624
|
+
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
11625
|
+
deliverRuntimeFile(
|
|
11626
|
+
options.container.id,
|
|
11627
|
+
runtimeAdapterPath,
|
|
11628
|
+
options.plan.adapterContents,
|
|
11629
|
+
"the managed post-start adapter"
|
|
11630
|
+
);
|
|
11631
|
+
const started2 = (0, import_node_child_process20.spawnSync)(
|
|
11632
|
+
"docker",
|
|
11633
|
+
[
|
|
11634
|
+
"exec",
|
|
11635
|
+
"--workdir",
|
|
11636
|
+
options.container.workspacePath,
|
|
11637
|
+
"--env",
|
|
11638
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
11639
|
+
"--env",
|
|
11640
|
+
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
11641
|
+
"--env",
|
|
11642
|
+
`DEVROUTER_PROCESS_HOST_PORTS_SHA256=${portIdentity}`,
|
|
11643
|
+
...Object.entries(portEnvironment).flatMap(([name, value]) => ["--env", `${name}=${value}`]),
|
|
11644
|
+
"--env",
|
|
11645
|
+
`DEVROUTER_RUNTIME_VARIANT=${runtimeVariant}`,
|
|
11646
|
+
...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
|
|
11647
|
+
...options.processes ? [
|
|
11648
|
+
"--env",
|
|
11649
|
+
`DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
|
|
11650
|
+
"--env",
|
|
11651
|
+
"DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
|
|
11652
|
+
] : [],
|
|
11653
|
+
options.container.id,
|
|
11654
|
+
"bash",
|
|
11655
|
+
"-c",
|
|
11656
|
+
ADAPTER_WRAPPER,
|
|
11657
|
+
options.plan.adapterPath,
|
|
11658
|
+
runtimeAdapterPath
|
|
11659
|
+
],
|
|
11660
|
+
{
|
|
11661
|
+
stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit",
|
|
11662
|
+
...networkDockerOptions()
|
|
11663
|
+
}
|
|
11664
|
+
);
|
|
11665
|
+
if (started2.status !== 0) {
|
|
11666
|
+
const details = commandFailure2(started2);
|
|
11667
|
+
throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
|
|
11668
|
+
}
|
|
11669
|
+
}
|
|
11670
|
+
function assertSafeProcessName(name) {
|
|
11671
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
11672
|
+
throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
|
|
11673
|
+
}
|
|
11674
|
+
}
|
|
11675
|
+
function runManagedProcessAction(options) {
|
|
11676
|
+
assertSafeProcessName(options.name);
|
|
11677
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
11678
|
+
"docker",
|
|
11679
|
+
[
|
|
11680
|
+
"exec",
|
|
11681
|
+
"--workdir",
|
|
11682
|
+
options.container.workspacePath,
|
|
11683
|
+
"--env",
|
|
11684
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
11685
|
+
options.container.id,
|
|
11686
|
+
"bash",
|
|
11687
|
+
"-c",
|
|
11688
|
+
'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
|
|
11689
|
+
"devrouter-process",
|
|
11690
|
+
options.action,
|
|
11691
|
+
options.name
|
|
11692
|
+
],
|
|
11693
|
+
{
|
|
11694
|
+
encoding: "utf-8",
|
|
11695
|
+
stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"],
|
|
11696
|
+
...networkDockerOptions()
|
|
11697
|
+
}
|
|
11698
|
+
);
|
|
11699
|
+
if (options.action === "stop") {
|
|
11700
|
+
if (result.status !== 0) {
|
|
11701
|
+
throw new Error(`Could not stop managed process '${options.name}'.`);
|
|
11702
|
+
}
|
|
11703
|
+
return "stopped";
|
|
11704
|
+
}
|
|
11705
|
+
try {
|
|
11706
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
11707
|
+
if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
|
|
11708
|
+
return parsed.status;
|
|
11709
|
+
}
|
|
11710
|
+
} catch {
|
|
11711
|
+
}
|
|
11712
|
+
return "drifted";
|
|
11713
|
+
}
|
|
11714
|
+
|
|
11715
|
+
// src/core/recovery-budget.ts
|
|
11716
|
+
function findRecoveryUnit(incident, unit) {
|
|
11717
|
+
return (incident.units ?? []).find((record5) => record5.key === unit.key);
|
|
11718
|
+
}
|
|
11719
|
+
function restartsFor(kind, limits) {
|
|
11720
|
+
return kind === "service" ? limits.maxServiceRestarts : limits.maxProcessRestarts;
|
|
11721
|
+
}
|
|
11722
|
+
function recoveryBudgetRefusal(input) {
|
|
11723
|
+
const startedAtMs = input.incident.startedAtMs ?? input.nowMs;
|
|
11724
|
+
const elapsed = input.activeElapsedMs ?? Math.max(0, input.nowMs - startedAtMs);
|
|
11725
|
+
if (elapsed > input.limits.windowSeconds * 1e3) return "window-closed";
|
|
11726
|
+
if (input.unit !== void 0) {
|
|
11727
|
+
const record5 = findRecoveryUnit(input.incident, input.unit);
|
|
11728
|
+
if (record5 !== void 0 && record5.actions >= restartsFor(input.unit.kind, input.limits))
|
|
11729
|
+
return "unit-exhausted";
|
|
11730
|
+
}
|
|
11731
|
+
if (input.incident.correctiveActionsTaken >= input.incident.actionLimit)
|
|
11732
|
+
return "budget-exhausted";
|
|
11733
|
+
return void 0;
|
|
11734
|
+
}
|
|
11735
|
+
function claimRecoveryUnit(input) {
|
|
11736
|
+
const reason2 = recoveryBudgetRefusal(input);
|
|
11737
|
+
if (reason2 !== void 0) return { ok: false, reason: reason2 };
|
|
11738
|
+
const units = (input.incident.units ?? []).map((record5) => ({ ...record5 }));
|
|
11739
|
+
if (input.unit !== void 0) {
|
|
11740
|
+
const index = units.findIndex((record5) => record5.key === input.unit?.key);
|
|
11741
|
+
if (index === -1) {
|
|
11742
|
+
if (units.length >= RELIABILITY_MAX_ITEMS) return { ok: false, reason: "unit-exhausted" };
|
|
11743
|
+
units.push({ ...input.unit, actions: 1, lastActionAtMs: input.nowMs });
|
|
11744
|
+
} else {
|
|
11745
|
+
const record5 = units[index];
|
|
11746
|
+
units[index] = {
|
|
11747
|
+
...record5,
|
|
11748
|
+
actions: record5.actions + 1,
|
|
11749
|
+
lastActionAtMs: input.nowMs
|
|
11750
|
+
};
|
|
11751
|
+
}
|
|
11752
|
+
}
|
|
11753
|
+
return {
|
|
11754
|
+
ok: true,
|
|
11755
|
+
incident: {
|
|
11756
|
+
...input.incident,
|
|
11757
|
+
startedAtMs: input.incident.startedAtMs ?? input.nowMs,
|
|
11758
|
+
correctiveActionsTaken: input.incident.correctiveActionsTaken + 1,
|
|
11759
|
+
units
|
|
11760
|
+
}
|
|
11761
|
+
};
|
|
11762
|
+
}
|
|
11763
|
+
|
|
11764
|
+
// src/core/reliability-model.ts
|
|
11765
|
+
var INFRASTRUCTURE_VALUES = ["healthy", "failed", "unknown"];
|
|
11766
|
+
var APPLICATION_VALUES = ["verified", "unready", "unverified"];
|
|
11767
|
+
var ADMISSION_VALUES = ["admitted", "waiting", "unknown", "denied-unadmittable"];
|
|
11768
|
+
function record3(value) {
|
|
11769
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11770
|
+
}
|
|
11771
|
+
function oneOf2(value, values) {
|
|
11772
|
+
return typeof value === "string" && values.includes(value);
|
|
11773
|
+
}
|
|
11774
|
+
function uniqueIds2(values) {
|
|
11775
|
+
return new Set(values).size === values.length && values.every(isReliabilityId);
|
|
11776
|
+
}
|
|
11777
|
+
function isFence(value) {
|
|
11778
|
+
return record3(value) && isReliabilityId(value.environmentId) && isReliabilityCounter(value.intentRevision) && isReliabilityCounter(value.runtimeGeneration) && isReliabilityCounter(value.controllerEpoch);
|
|
11779
|
+
}
|
|
11780
|
+
function isConsumer(value) {
|
|
11781
|
+
return record3(value) && isReliabilityId(value.id) && typeof value.pinned === "boolean" && Array.isArray(value.requiredCapabilities) && value.requiredCapabilities.length <= 128 && uniqueIds2(value.requiredCapabilities);
|
|
11782
|
+
}
|
|
11783
|
+
function isObservation(value) {
|
|
11784
|
+
return record3(value) && isReliabilityId(value.capability) && oneOf2(value.infrastructure, INFRASTRUCTURE_VALUES) && oneOf2(value.application, APPLICATION_VALUES) && isReliabilityCounter(value.observedAtMs) && isReliabilityCounter(value.validForMs);
|
|
11785
|
+
}
|
|
11786
|
+
function assertReliabilityEvent(value) {
|
|
11787
|
+
if (!record3(value) || !isFence(value) || typeof value.type !== "string") {
|
|
11788
|
+
throw new Error("Invalid reliability event.");
|
|
11789
|
+
}
|
|
11790
|
+
let valid = false;
|
|
11791
|
+
switch (value.type) {
|
|
11792
|
+
case "request":
|
|
11793
|
+
valid = oneOf2(value.mode, ["start", "attach"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer);
|
|
11794
|
+
break;
|
|
11795
|
+
case "operation-request":
|
|
11796
|
+
valid = oneOf2(value.kind, ["ensure", "exec"]) && isReliabilityId(value.key) && isReliabilityId(value.operationId) && isReliabilityProfile(value.profile) && isConsumer(value.consumer) && typeof value.runtimeRunning === "boolean" && (value.recoverInterruptedEnsure === void 0 || typeof value.recoverInterruptedEnsure === "boolean");
|
|
11797
|
+
break;
|
|
11798
|
+
case "drained":
|
|
11799
|
+
valid = isReliabilityId(value.operationId);
|
|
11800
|
+
break;
|
|
11801
|
+
case "release":
|
|
11802
|
+
valid = isReliabilityId(value.consumerId);
|
|
11803
|
+
break;
|
|
11804
|
+
case "stop":
|
|
11805
|
+
case "park":
|
|
11806
|
+
case "dispatch":
|
|
11807
|
+
valid = true;
|
|
11808
|
+
break;
|
|
11809
|
+
case "admission":
|
|
11810
|
+
valid = oneOf2(value.result, ADMISSION_VALUES);
|
|
11811
|
+
break;
|
|
10608
11812
|
case "resume":
|
|
10609
11813
|
valid = typeof value.pressureDwellSatisfied === "boolean" && isReliabilityId(value.operationId);
|
|
10610
11814
|
break;
|
|
@@ -11276,10 +12480,10 @@ function stepReliability(state, event, nowMs) {
|
|
|
11276
12480
|
}
|
|
11277
12481
|
|
|
11278
12482
|
// src/core/reliability-worker.ts
|
|
11279
|
-
var
|
|
11280
|
-
var
|
|
11281
|
-
var
|
|
11282
|
-
var
|
|
12483
|
+
var import_node_child_process21 = require("child_process");
|
|
12484
|
+
var import_node_crypto19 = require("crypto");
|
|
12485
|
+
var import_node_fs30 = __toESM(require("fs"));
|
|
12486
|
+
var import_node_path35 = __toESM(require("path"));
|
|
11283
12487
|
|
|
11284
12488
|
// src/core/lifecycle-progress.ts
|
|
11285
12489
|
var ROLES = {
|
|
@@ -11345,11 +12549,11 @@ function workerGroupAbsent(pid) {
|
|
|
11345
12549
|
if (process.platform !== "linux") return false;
|
|
11346
12550
|
let observedZombie = false;
|
|
11347
12551
|
try {
|
|
11348
|
-
for (const entry of
|
|
12552
|
+
for (const entry of import_node_fs30.default.readdirSync("/proc")) {
|
|
11349
12553
|
if (!/^\d+$/.test(entry)) continue;
|
|
11350
12554
|
let stat;
|
|
11351
12555
|
try {
|
|
11352
|
-
stat =
|
|
12556
|
+
stat = import_node_fs30.default.readFileSync(`/proc/${entry}/stat`, "utf8");
|
|
11353
12557
|
} catch (error) {
|
|
11354
12558
|
if (error.code === "ENOENT") continue;
|
|
11355
12559
|
return false;
|
|
@@ -11369,20 +12573,20 @@ function workerGroupAbsent(pid) {
|
|
|
11369
12573
|
}
|
|
11370
12574
|
|
|
11371
12575
|
// src/core/workspace-ensure.ts
|
|
11372
|
-
var
|
|
12576
|
+
var import_node_child_process31 = require("child_process");
|
|
11373
12577
|
var import_node_fs35 = __toESM(require("fs"));
|
|
11374
12578
|
var import_node_path40 = __toESM(require("path"));
|
|
11375
12579
|
|
|
11376
12580
|
// src/core/devpod-mutation.ts
|
|
11377
|
-
var
|
|
11378
|
-
var
|
|
11379
|
-
var
|
|
11380
|
-
var DEVPOD_MUTATION_LOCK_FILE =
|
|
12581
|
+
var import_node_child_process22 = require("child_process");
|
|
12582
|
+
var import_node_fs31 = __toESM(require("fs"));
|
|
12583
|
+
var import_node_path36 = __toESM(require("path"));
|
|
12584
|
+
var DEVPOD_MUTATION_LOCK_FILE = import_node_path36.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
11381
12585
|
var DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
11382
12586
|
var DevpodStartPostconditionError = class extends Error {
|
|
11383
12587
|
};
|
|
11384
12588
|
function withMutationLock2(activity, target, operation) {
|
|
11385
|
-
|
|
12589
|
+
import_node_fs31.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
11386
12590
|
return withFileLockSync(
|
|
11387
12591
|
DEVPOD_MUTATION_LOCK_FILE,
|
|
11388
12592
|
{
|
|
@@ -11395,20 +12599,20 @@ function withMutationLock2(activity, target, operation) {
|
|
|
11395
12599
|
operation
|
|
11396
12600
|
);
|
|
11397
12601
|
}
|
|
11398
|
-
function
|
|
12602
|
+
function commandFailure3(result) {
|
|
11399
12603
|
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
11400
12604
|
}
|
|
11401
12605
|
function runDevpodAction(action, devpodId, force = false, repoPath) {
|
|
11402
12606
|
const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
|
|
11403
12607
|
claimLifecycleEffect();
|
|
11404
12608
|
const binding = repoPath ? networkProviderEffectOptions("devpod", devpodId, repoPath) : { args: [] };
|
|
11405
|
-
const result = (0,
|
|
12609
|
+
const result = (0, import_node_child_process22.spawnSync)("devpod", [...args, ...binding.args], {
|
|
11406
12610
|
encoding: "utf-8",
|
|
11407
12611
|
...binding.env ? { env: binding.env } : {}
|
|
11408
12612
|
});
|
|
11409
12613
|
if (result.status !== 0) {
|
|
11410
12614
|
throw new Error(
|
|
11411
|
-
`devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${
|
|
12615
|
+
`devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure3(result) || "unknown error"}`
|
|
11412
12616
|
);
|
|
11413
12617
|
}
|
|
11414
12618
|
}
|
|
@@ -11543,7 +12747,7 @@ async function startDevpodWorkspace(options) {
|
|
|
11543
12747
|
}
|
|
11544
12748
|
claimLifecycleEffect();
|
|
11545
12749
|
network?.beforeDispatch?.();
|
|
11546
|
-
const result = (0,
|
|
12750
|
+
const result = (0, import_node_child_process22.spawnSync)("devpod", args, {
|
|
11547
12751
|
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
11548
12752
|
env
|
|
11549
12753
|
});
|
|
@@ -11583,8 +12787,8 @@ async function startDevpodWorkspace(options) {
|
|
|
11583
12787
|
}
|
|
11584
12788
|
|
|
11585
12789
|
// src/core/host-port-claims.ts
|
|
11586
|
-
var
|
|
11587
|
-
var
|
|
12790
|
+
var import_node_child_process23 = require("child_process");
|
|
12791
|
+
var import_node_path37 = __toESM(require("path"));
|
|
11588
12792
|
var COMPOSE_RENDER_TIMEOUT_MS = 3e4;
|
|
11589
12793
|
var COMPOSE_RENDER_MAX_BUFFER = 8 * 1024 * 1024;
|
|
11590
12794
|
var DOCKER_TIMEOUT_MS = 1e4;
|
|
@@ -11687,7 +12891,7 @@ function hostPortBindingsConflict(desired, holder) {
|
|
|
11687
12891
|
}
|
|
11688
12892
|
var HOST_PORT_INSPECT_TEMPLATE = '{"id":{{json .Id}},"name":{{json .Name}},"labels":{"com.docker.compose.project":{{json (index .Config.Labels "com.docker.compose.project")}},"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}}},"ports":{{json .NetworkSettings.Ports}}}';
|
|
11689
12893
|
function runDocker(args) {
|
|
11690
|
-
const result = (0,
|
|
12894
|
+
const result = (0, import_node_child_process23.spawnSync)("docker", args, {
|
|
11691
12895
|
...networkDockerOptions(),
|
|
11692
12896
|
encoding: "utf-8",
|
|
11693
12897
|
timeout: DOCKER_TIMEOUT_MS,
|
|
@@ -11757,7 +12961,7 @@ function listHolderSnapshots() {
|
|
|
11757
12961
|
}
|
|
11758
12962
|
function renderManagedComposeModel(plan, workspace) {
|
|
11759
12963
|
const fileArgs = plan.composeFiles.flatMap((file) => ["-f", file]);
|
|
11760
|
-
const result = (0,
|
|
12964
|
+
const result = (0, import_node_child_process23.spawnSync)(
|
|
11761
12965
|
"docker",
|
|
11762
12966
|
[
|
|
11763
12967
|
"compose",
|
|
@@ -11792,8 +12996,8 @@ function renderManagedComposeModel(plan, workspace) {
|
|
|
11792
12996
|
}
|
|
11793
12997
|
}
|
|
11794
12998
|
function attributeHolderWorktree(workingDir) {
|
|
11795
|
-
if (
|
|
11796
|
-
return comparableWorkspacePath(
|
|
12999
|
+
if (import_node_path37.default.basename(workingDir) !== ".devcontainer") return void 0;
|
|
13000
|
+
return comparableWorkspacePath(import_node_path37.default.dirname(workingDir));
|
|
11797
13001
|
}
|
|
11798
13002
|
function holderAttribution(holder, repoPath) {
|
|
11799
13003
|
const attribution = {};
|
|
@@ -11828,7 +13032,7 @@ function detectHostPortClaimConflicts(options) {
|
|
|
11828
13032
|
);
|
|
11829
13033
|
if (desired.length === 0) return [];
|
|
11830
13034
|
const holders = listHolderSnapshots();
|
|
11831
|
-
const devcontainerDir =
|
|
13035
|
+
const devcontainerDir = import_node_path37.default.join(options.repoPath, ".devcontainer");
|
|
11832
13036
|
const conflicts = [];
|
|
11833
13037
|
for (const binding of desired) {
|
|
11834
13038
|
for (const holder of holders) {
|
|
@@ -11852,7 +13056,7 @@ function detectHostPortClaimConflicts(options) {
|
|
|
11852
13056
|
}
|
|
11853
13057
|
|
|
11854
13058
|
// src/core/http-route-probe.ts
|
|
11855
|
-
var
|
|
13059
|
+
var import_node_child_process24 = require("child_process");
|
|
11856
13060
|
var DEFAULT_MAX_TIME_SECONDS = 5;
|
|
11857
13061
|
var PARENT_TIMEOUT_PADDING_MS = 250;
|
|
11858
13062
|
var CONTRACT_MAX_BUFFER = 1024;
|
|
@@ -11885,7 +13089,7 @@ function probeHttpRoute(host, options = {}) {
|
|
|
11885
13089
|
args.push("--cacert", CERT_FILE);
|
|
11886
13090
|
}
|
|
11887
13091
|
args.push(url);
|
|
11888
|
-
const result = (0,
|
|
13092
|
+
const result = (0, import_node_child_process24.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
11889
13093
|
const stdout = result.stdout?.trim() ?? "";
|
|
11890
13094
|
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
11891
13095
|
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
@@ -11904,7 +13108,7 @@ function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
|
|
|
11904
13108
|
}
|
|
11905
13109
|
let result;
|
|
11906
13110
|
try {
|
|
11907
|
-
result = (0,
|
|
13111
|
+
result = (0, import_node_child_process24.spawnSync)("curl", args, {
|
|
11908
13112
|
encoding: "utf-8",
|
|
11909
13113
|
maxBuffer: CONTRACT_MAX_BUFFER,
|
|
11910
13114
|
timeout: Math.ceil(parentTimeoutMs)
|
|
@@ -11988,8 +13192,8 @@ function transportFailure() {
|
|
|
11988
13192
|
}
|
|
11989
13193
|
|
|
11990
13194
|
// src/core/managed-host-preparation.ts
|
|
11991
|
-
var
|
|
11992
|
-
var
|
|
13195
|
+
var import_node_child_process25 = require("child_process");
|
|
13196
|
+
var import_node_fs32 = __toESM(require("fs"));
|
|
11993
13197
|
var PREPARATION_TIMEOUT_MS = 6e4;
|
|
11994
13198
|
var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
|
|
11995
13199
|
var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
|
|
@@ -12000,7 +13204,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
12000
13204
|
return new Promise((resolve) => {
|
|
12001
13205
|
let child;
|
|
12002
13206
|
try {
|
|
12003
|
-
child = (0,
|
|
13207
|
+
child = (0, import_node_child_process25.spawn)(argv[0], argv.slice(1), {
|
|
12004
13208
|
cwd,
|
|
12005
13209
|
detached: true,
|
|
12006
13210
|
shell: false,
|
|
@@ -12032,7 +13236,7 @@ function runPreparationCommand(argv, cwd) {
|
|
|
12032
13236
|
}
|
|
12033
13237
|
function readConfigBytes(configPath) {
|
|
12034
13238
|
try {
|
|
12035
|
-
return
|
|
13239
|
+
return import_node_fs32.default.readFileSync(configPath);
|
|
12036
13240
|
} catch {
|
|
12037
13241
|
return void 0;
|
|
12038
13242
|
}
|
|
@@ -12065,219 +13269,8 @@ async function runManagedHostPreparation(repoPath, config) {
|
|
|
12065
13269
|
}
|
|
12066
13270
|
}
|
|
12067
13271
|
|
|
12068
|
-
// src/core/managed-post-start.ts
|
|
12069
|
-
var import_node_child_process24 = require("child_process");
|
|
12070
|
-
var import_node_crypto18 = require("crypto");
|
|
12071
|
-
var import_node_fs32 = __toESM(require("fs"));
|
|
12072
|
-
var import_node_path37 = __toESM(require("path"));
|
|
12073
|
-
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
12074
|
-
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
12075
|
-
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
12076
|
-
var ADAPTER_WRAPPER = `adapter_snapshot="$1"
|
|
12077
|
-
shift
|
|
12078
|
-
readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256
|
|
12079
|
-
source "$adapter_snapshot"
|
|
12080
|
-
`;
|
|
12081
|
-
function renderDeliveryScript(targetPath) {
|
|
12082
|
-
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
12083
|
-
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
12084
|
-
}
|
|
12085
|
-
const temporaryPrefix = import_node_path37.default.posix.basename(targetPath);
|
|
12086
|
-
return `set -eu
|
|
12087
|
-
umask 077
|
|
12088
|
-
runtime_root=/tmp/devrouter
|
|
12089
|
-
runtime_bin="$runtime_root/bin"
|
|
12090
|
-
target="${targetPath}"
|
|
12091
|
-
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
12092
|
-
echo "Refusing symlinked devrouter runtime path." >&2
|
|
12093
|
-
exit 1
|
|
12094
|
-
fi
|
|
12095
|
-
mkdir -p "$runtime_bin"
|
|
12096
|
-
chmod 700 "$runtime_root" "$runtime_bin"
|
|
12097
|
-
temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
|
|
12098
|
-
trap 'rm -f "$temporary"' EXIT
|
|
12099
|
-
cat > "$temporary"
|
|
12100
|
-
chmod 700 "$temporary"
|
|
12101
|
-
mv -f "$temporary" "$target"
|
|
12102
|
-
trap - EXIT
|
|
12103
|
-
`;
|
|
12104
|
-
}
|
|
12105
|
-
function commandFailure3(result) {
|
|
12106
|
-
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
12107
|
-
}
|
|
12108
|
-
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
12109
|
-
const result = (0, import_node_child_process24.spawnSync)(
|
|
12110
|
-
"docker",
|
|
12111
|
-
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
12112
|
-
{ input: contents, encoding: "utf-8", ...networkDockerOptions() }
|
|
12113
|
-
);
|
|
12114
|
-
if (result.status !== 0) {
|
|
12115
|
-
const details = commandFailure3(result);
|
|
12116
|
-
throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
|
|
12117
|
-
}
|
|
12118
|
-
}
|
|
12119
|
-
function readRegularFileBytes(filePath) {
|
|
12120
|
-
if (!import_node_fs32.default.existsSync(filePath) || !import_node_fs32.default.lstatSync(filePath).isFile()) return void 0;
|
|
12121
|
-
return import_node_fs32.default.readFileSync(filePath);
|
|
12122
|
-
}
|
|
12123
|
-
function readRegularFile(filePath) {
|
|
12124
|
-
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
12125
|
-
}
|
|
12126
|
-
function adapterFingerprint(adapter) {
|
|
12127
|
-
return (0, import_node_crypto18.createHash)("sha256").update(adapter).digest("hex");
|
|
12128
|
-
}
|
|
12129
|
-
function resolveProcessHelperPath() {
|
|
12130
|
-
const candidates = [
|
|
12131
|
-
import_node_path37.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
12132
|
-
import_node_path37.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
12133
|
-
];
|
|
12134
|
-
const helperPath = candidates.find((candidate) => import_node_fs32.default.existsSync(candidate));
|
|
12135
|
-
if (!helperPath) {
|
|
12136
|
-
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
12137
|
-
}
|
|
12138
|
-
return helperPath;
|
|
12139
|
-
}
|
|
12140
|
-
function resolveManagedPostStartPlan(repoPath) {
|
|
12141
|
-
const adapterPath = import_node_path37.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
12142
|
-
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
12143
|
-
const adapterText = adapterBytes?.toString("utf-8");
|
|
12144
|
-
const dockerfilePath = import_node_path37.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
12145
|
-
const devcontainerPath = import_node_path37.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
12146
|
-
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
12147
|
-
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
12148
|
-
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
12149
|
-
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
12150
|
-
);
|
|
12151
|
-
if (adapterBytes === void 0) {
|
|
12152
|
-
if (devrouterPattern) {
|
|
12153
|
-
throw new Error(
|
|
12154
|
-
"Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
|
|
12155
|
-
);
|
|
12156
|
-
}
|
|
12157
|
-
return { kind: "unmanaged" };
|
|
12158
|
-
}
|
|
12159
|
-
const adapter = adapterBytes.toString("utf-8");
|
|
12160
|
-
if (!adapter.includes(MANAGED_MARKER)) {
|
|
12161
|
-
if (devrouterPattern) {
|
|
12162
|
-
throw new Error(
|
|
12163
|
-
"Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
12164
|
-
);
|
|
12165
|
-
}
|
|
12166
|
-
return { kind: "unmanaged" };
|
|
12167
|
-
}
|
|
12168
|
-
assertManagedDevcontainerLifecycle(repoPath);
|
|
12169
|
-
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
12170
|
-
return {
|
|
12171
|
-
kind: "runtime",
|
|
12172
|
-
adapterPath: MANAGED_ADAPTER_PATH,
|
|
12173
|
-
adapterSha256: adapterFingerprint(adapterBytes),
|
|
12174
|
-
adapterContents: adapterBytes
|
|
12175
|
-
};
|
|
12176
|
-
}
|
|
12177
|
-
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
12178
|
-
return { kind: "legacy" };
|
|
12179
|
-
}
|
|
12180
|
-
throw new Error(
|
|
12181
|
-
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
12182
|
-
);
|
|
12183
|
-
}
|
|
12184
|
-
function runManagedPostStart(options) {
|
|
12185
|
-
if (options.plan.kind !== "runtime") return;
|
|
12186
|
-
const helper = import_node_fs32.default.readFileSync(resolveProcessHelperPath());
|
|
12187
|
-
deliverRuntimeFile(
|
|
12188
|
-
options.container.id,
|
|
12189
|
-
RUNTIME_HELPER_PATH,
|
|
12190
|
-
helper,
|
|
12191
|
-
"the managed process helper"
|
|
12192
|
-
);
|
|
12193
|
-
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
12194
|
-
deliverRuntimeFile(
|
|
12195
|
-
options.container.id,
|
|
12196
|
-
runtimeAdapterPath,
|
|
12197
|
-
options.plan.adapterContents,
|
|
12198
|
-
"the managed post-start adapter"
|
|
12199
|
-
);
|
|
12200
|
-
const started2 = (0, import_node_child_process24.spawnSync)(
|
|
12201
|
-
"docker",
|
|
12202
|
-
[
|
|
12203
|
-
"exec",
|
|
12204
|
-
"--workdir",
|
|
12205
|
-
options.container.workspacePath,
|
|
12206
|
-
"--env",
|
|
12207
|
-
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
12208
|
-
"--env",
|
|
12209
|
-
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
12210
|
-
...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
|
|
12211
|
-
...options.processes ? [
|
|
12212
|
-
"--env",
|
|
12213
|
-
`DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
|
|
12214
|
-
"--env",
|
|
12215
|
-
"DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
|
|
12216
|
-
] : [],
|
|
12217
|
-
options.container.id,
|
|
12218
|
-
"bash",
|
|
12219
|
-
"-c",
|
|
12220
|
-
ADAPTER_WRAPPER,
|
|
12221
|
-
options.plan.adapterPath,
|
|
12222
|
-
runtimeAdapterPath
|
|
12223
|
-
],
|
|
12224
|
-
{
|
|
12225
|
-
stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit",
|
|
12226
|
-
...networkDockerOptions()
|
|
12227
|
-
}
|
|
12228
|
-
);
|
|
12229
|
-
if (started2.status !== 0) {
|
|
12230
|
-
const details = commandFailure3(started2);
|
|
12231
|
-
throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
|
|
12232
|
-
}
|
|
12233
|
-
}
|
|
12234
|
-
function assertSafeProcessName(name) {
|
|
12235
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
12236
|
-
throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
|
|
12237
|
-
}
|
|
12238
|
-
}
|
|
12239
|
-
function runManagedProcessAction(options) {
|
|
12240
|
-
assertSafeProcessName(options.name);
|
|
12241
|
-
const result = (0, import_node_child_process24.spawnSync)(
|
|
12242
|
-
"docker",
|
|
12243
|
-
[
|
|
12244
|
-
"exec",
|
|
12245
|
-
"--workdir",
|
|
12246
|
-
options.container.workspacePath,
|
|
12247
|
-
"--env",
|
|
12248
|
-
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
12249
|
-
options.container.id,
|
|
12250
|
-
"bash",
|
|
12251
|
-
"-c",
|
|
12252
|
-
'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
|
|
12253
|
-
"devrouter-process",
|
|
12254
|
-
options.action,
|
|
12255
|
-
options.name
|
|
12256
|
-
],
|
|
12257
|
-
{
|
|
12258
|
-
encoding: "utf-8",
|
|
12259
|
-
stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"],
|
|
12260
|
-
...networkDockerOptions()
|
|
12261
|
-
}
|
|
12262
|
-
);
|
|
12263
|
-
if (options.action === "stop") {
|
|
12264
|
-
if (result.status !== 0) {
|
|
12265
|
-
throw new Error(`Could not stop managed process '${options.name}'.`);
|
|
12266
|
-
}
|
|
12267
|
-
return "stopped";
|
|
12268
|
-
}
|
|
12269
|
-
try {
|
|
12270
|
-
const parsed = JSON.parse(result.stdout.trim());
|
|
12271
|
-
if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
|
|
12272
|
-
return parsed.status;
|
|
12273
|
-
}
|
|
12274
|
-
} catch {
|
|
12275
|
-
}
|
|
12276
|
-
return "drifted";
|
|
12277
|
-
}
|
|
12278
|
-
|
|
12279
13272
|
// src/core/managed-mount-nesting.ts
|
|
12280
|
-
var
|
|
13273
|
+
var import_node_child_process26 = require("child_process");
|
|
12281
13274
|
var NESTED_MOUNT_REASON = "no configured mount is nested inside another configured mount";
|
|
12282
13275
|
var UNREADABLE_MOUNT_REASON = "the container's own mount table could not be read";
|
|
12283
13276
|
function normalizeDestination(value) {
|
|
@@ -12350,7 +13343,7 @@ function classifyManagedMountNesting(configured, effectiveDestinations, containe
|
|
|
12350
13343
|
};
|
|
12351
13344
|
}
|
|
12352
13345
|
function readContainerMountinfo(containerId, options) {
|
|
12353
|
-
const result = (0,
|
|
13346
|
+
const result = (0, import_node_child_process26.spawnSync)("docker", ["exec", containerId, "cat", "/proc/self/mountinfo"], {
|
|
12354
13347
|
encoding: "utf-8",
|
|
12355
13348
|
timeout: options.timeoutMs ?? 5e3,
|
|
12356
13349
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -12371,8 +13364,9 @@ function recoveryPath(repoPath) {
|
|
|
12371
13364
|
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(repoPath)) return repoPath;
|
|
12372
13365
|
return `'${repoPath.replaceAll("'", "'\\''")}'`;
|
|
12373
13366
|
}
|
|
12374
|
-
function reliabilityRecovery(reason2, repoPath) {
|
|
13367
|
+
function reliabilityRecovery(reason2, repoPath, variant) {
|
|
12375
13368
|
const checkout = recoveryPath(repoPath);
|
|
13369
|
+
const variantOption = variant && variant !== "default" ? ` --variant ${variant}` : "";
|
|
12376
13370
|
switch (reason2) {
|
|
12377
13371
|
case "stop-incomplete":
|
|
12378
13372
|
case "operation-unknown":
|
|
@@ -12380,12 +13374,12 @@ function reliabilityRecovery(reason2, repoPath) {
|
|
|
12380
13374
|
case "start-refused":
|
|
12381
13375
|
return [
|
|
12382
13376
|
`Run: devrouter doctor --repo ${checkout} to read the refused fixed host-port claim and its holder`,
|
|
12383
|
-
`Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${checkout}`
|
|
13377
|
+
`Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${checkout}${variantOption}`
|
|
12384
13378
|
];
|
|
12385
13379
|
case "unadmittable":
|
|
12386
13380
|
return [
|
|
12387
13381
|
`Run: devrouter stop ${checkout}`,
|
|
12388
|
-
`Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${checkout}`
|
|
13382
|
+
`Lower the requested capacity in .devrouter.yml, then run: devrouter ensure ${checkout}${variantOption}`
|
|
12389
13383
|
];
|
|
12390
13384
|
case "capacity-parked":
|
|
12391
13385
|
return [
|
|
@@ -12394,7 +13388,9 @@ function reliabilityRecovery(reason2, repoPath) {
|
|
|
12394
13388
|
`'devrouter ensure' refuses while the journal stays parked; release the intent instead with: devrouter stop ${checkout}`
|
|
12395
13389
|
];
|
|
12396
13390
|
case "capacity-waiting":
|
|
12397
|
-
return [
|
|
13391
|
+
return [
|
|
13392
|
+
`Run: devrouter ensure ${checkout}${variantOption} (still queued until capacity frees up)`
|
|
13393
|
+
];
|
|
12398
13394
|
default:
|
|
12399
13395
|
return [];
|
|
12400
13396
|
}
|
|
@@ -12435,7 +13431,12 @@ function readReliabilityStatus(options) {
|
|
|
12435
13431
|
correctiveActionsTaken: record5.state.incident.correctiveActionsTaken,
|
|
12436
13432
|
actionLimit: record5.state.incident.actionLimit
|
|
12437
13433
|
} : null,
|
|
12438
|
-
...reason2 ? {
|
|
13434
|
+
...reason2 ? {
|
|
13435
|
+
attention: {
|
|
13436
|
+
reason: reason2,
|
|
13437
|
+
recovery: reliabilityRecovery(reason2, options.repoPath, options.variant)
|
|
13438
|
+
}
|
|
13439
|
+
} : {}
|
|
12439
13440
|
};
|
|
12440
13441
|
}
|
|
12441
13442
|
var EMPTY_RESOURCES = {
|
|
@@ -12760,6 +13761,10 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12760
13761
|
}
|
|
12761
13762
|
const drift = [];
|
|
12762
13763
|
if (stateReadFailed) drift.push("managed runtime state could not be read");
|
|
13764
|
+
if (state && options.variant !== void 0 && (state.variant ?? "default") !== options.variant)
|
|
13765
|
+
drift.push("active runtime variant differs from the selected variant");
|
|
13766
|
+
if (state?.variant && state.variant !== "default" && !managedRuntime.variants?.includes(state.variant))
|
|
13767
|
+
drift.push("active runtime variant is no longer declared");
|
|
12763
13768
|
if (state?.status === "degraded") drift.push("the last managed transition is degraded");
|
|
12764
13769
|
if (state) {
|
|
12765
13770
|
const desiredState = {
|
|
@@ -12841,6 +13846,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12841
13846
|
};
|
|
12842
13847
|
const reliability = readReliabilityStatus({
|
|
12843
13848
|
repoPath: options.repoPath,
|
|
13849
|
+
variant: state?.variant,
|
|
12844
13850
|
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
12845
13851
|
});
|
|
12846
13852
|
return {
|
|
@@ -12848,7 +13854,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12848
13854
|
status,
|
|
12849
13855
|
profile: options.profile,
|
|
12850
13856
|
...reliability ? { reliability } : {},
|
|
12851
|
-
...state?.profile ? { activeProfile: state.profile } : {},
|
|
13857
|
+
...state?.profile ? { activeProfile: state.profile, activeVariant: state.variant ?? "default" } : {},
|
|
12852
13858
|
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
12853
13859
|
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
12854
13860
|
...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
|
|
@@ -12872,7 +13878,7 @@ function collectManagedRuntimeStatus(options) {
|
|
|
12872
13878
|
}
|
|
12873
13879
|
|
|
12874
13880
|
// src/core/network-inventory.ts
|
|
12875
|
-
var
|
|
13881
|
+
var import_node_child_process27 = require("child_process");
|
|
12876
13882
|
var NetworkInventoryError = class extends Error {
|
|
12877
13883
|
};
|
|
12878
13884
|
var MAX_IDS = 4096;
|
|
@@ -12884,7 +13890,7 @@ function readDocker(endpoint, args, timeoutMs) {
|
|
|
12884
13890
|
const env = { ...process.env };
|
|
12885
13891
|
delete env.DOCKER_CONTEXT;
|
|
12886
13892
|
delete env.DOCKER_HOST;
|
|
12887
|
-
const result = (0,
|
|
13893
|
+
const result = (0, import_node_child_process27.spawnSync)("docker", ["--host", endpoint, ...args], {
|
|
12888
13894
|
env,
|
|
12889
13895
|
encoding: "utf-8",
|
|
12890
13896
|
timeout: timeoutMs,
|
|
@@ -13048,7 +14054,7 @@ function collectDockerNetworkInventory(options = {}, dependencies = {}) {
|
|
|
13048
14054
|
}
|
|
13049
14055
|
|
|
13050
14056
|
// src/core/network-routes.ts
|
|
13051
|
-
var
|
|
14057
|
+
var import_node_child_process28 = require("child_process");
|
|
13052
14058
|
var import_node_net3 = require("net");
|
|
13053
14059
|
|
|
13054
14060
|
// src/core/network-local-daemon.ts
|
|
@@ -13117,7 +14123,7 @@ function parseLinuxNetworkRoutes(output) {
|
|
|
13117
14123
|
function collectNetworkRoutes(options) {
|
|
13118
14124
|
if ((options.platform ?? process.platform) !== "linux" || !hasLocalDockerNetworkNamespace(options.endpoint))
|
|
13119
14125
|
return { status: "unknown", routes: [] };
|
|
13120
|
-
const result = (0,
|
|
14126
|
+
const result = (0, import_node_child_process28.spawnSync)("ip", ["-j", "-4", "route", "show", "table", "all"], {
|
|
13121
14127
|
encoding: "utf8",
|
|
13122
14128
|
timeout: 5e3,
|
|
13123
14129
|
maxBuffer: 1024 * 1024
|
|
@@ -13153,10 +14159,10 @@ function inspectLegacyNetworkCapacity(input) {
|
|
|
13153
14159
|
}
|
|
13154
14160
|
|
|
13155
14161
|
// src/core/network-managed.ts
|
|
13156
|
-
var
|
|
14162
|
+
var import_node_crypto22 = require("crypto");
|
|
13157
14163
|
|
|
13158
14164
|
// src/core/network-compose.ts
|
|
13159
|
-
var
|
|
14165
|
+
var import_node_crypto20 = require("crypto");
|
|
13160
14166
|
var SAFE_ENDPOINT_UPPER_BOUND_SEMANTICS = "full-lifecycle-demand";
|
|
13161
14167
|
function isRecord5(value) {
|
|
13162
14168
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -13962,12 +14968,12 @@ function stableValue(value) {
|
|
|
13962
14968
|
}
|
|
13963
14969
|
function fingerprintComposeNetworkModel(input) {
|
|
13964
14970
|
const projection = normalizedFingerprintProjection(input);
|
|
13965
|
-
return (0,
|
|
14971
|
+
return (0, import_node_crypto20.createHash)("sha256").update(stableValue(projection), "utf8").digest("hex");
|
|
13966
14972
|
}
|
|
13967
14973
|
|
|
13968
14974
|
// src/core/network-compose-files.ts
|
|
13969
|
-
var
|
|
13970
|
-
var
|
|
14975
|
+
var import_node_child_process29 = require("child_process");
|
|
14976
|
+
var import_node_crypto21 = require("crypto");
|
|
13971
14977
|
var import_node_fs34 = __toESM(require("fs"));
|
|
13972
14978
|
var import_node_path39 = __toESM(require("path"));
|
|
13973
14979
|
var import_yaml4 = __toESM(require("yaml"));
|
|
@@ -14003,7 +15009,7 @@ function inspectNetworkComposeFiles(input) {
|
|
|
14003
15009
|
const env = managedComposeEnvironment(input.workspace);
|
|
14004
15010
|
delete env.DOCKER_CONTEXT;
|
|
14005
15011
|
delete env.DOCKER_HOST;
|
|
14006
|
-
const result = (0,
|
|
15012
|
+
const result = (0, import_node_child_process29.spawnSync)(
|
|
14007
15013
|
"docker",
|
|
14008
15014
|
[
|
|
14009
15015
|
"--host",
|
|
@@ -14038,7 +15044,7 @@ function prepareNetworkComposeFiles(plan, subnet) {
|
|
|
14038
15044
|
const overlay = deriveComposeNetworkOverlay(subnet);
|
|
14039
15045
|
if (overlay.status !== "ready" || !overlay.overlay) throw new Error("Network subnet is invalid.");
|
|
14040
15046
|
const overlayPath = import_node_path39.default.join(plan.composeDirectory, OVERLAY);
|
|
14041
|
-
const ignored = (0,
|
|
15047
|
+
const ignored = (0, import_node_child_process29.spawnSync)("git", [
|
|
14042
15048
|
"-C",
|
|
14043
15049
|
plan.composeDirectory,
|
|
14044
15050
|
"check-ignore",
|
|
@@ -14068,7 +15074,7 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
14068
15074
|
...plan,
|
|
14069
15075
|
contents,
|
|
14070
15076
|
composeFiles: [...plan.composeFiles, overlayPath],
|
|
14071
|
-
effectiveConfigSha256: (0,
|
|
15077
|
+
effectiveConfigSha256: (0, import_node_crypto21.createHash)("sha256").update(contents).digest("hex")
|
|
14072
15078
|
};
|
|
14073
15079
|
return {
|
|
14074
15080
|
plan: result,
|
|
@@ -14080,7 +15086,7 @@ ${JSON.stringify(extended.config, null, 2)}
|
|
|
14080
15086
|
}
|
|
14081
15087
|
|
|
14082
15088
|
// src/core/network-connected-route.ts
|
|
14083
|
-
var
|
|
15089
|
+
var import_node_child_process30 = require("child_process");
|
|
14084
15090
|
function bounds(cidr) {
|
|
14085
15091
|
const canonical = parseIPv4Cidr(cidr);
|
|
14086
15092
|
if (!canonical) throw new Error("Invalid connected route.");
|
|
@@ -14105,7 +15111,7 @@ function inspectClaimedBridge(claim2, networkId) {
|
|
|
14105
15111
|
const env = { ...process.env };
|
|
14106
15112
|
delete env.DOCKER_CONTEXT;
|
|
14107
15113
|
delete env.DOCKER_HOST;
|
|
14108
|
-
const result = (0,
|
|
15114
|
+
const result = (0, import_node_child_process30.spawnSync)(
|
|
14109
15115
|
"docker",
|
|
14110
15116
|
[
|
|
14111
15117
|
"--host",
|
|
@@ -14132,7 +15138,7 @@ function inspectClaimedBridge(claim2, networkId) {
|
|
|
14132
15138
|
const [first, last] = bounds(claim2.subnet);
|
|
14133
15139
|
const [address] = bounds(gateway);
|
|
14134
15140
|
if (address <= first || address >= last) throw new Error();
|
|
14135
|
-
const interfaces = (0,
|
|
15141
|
+
const interfaces = (0, import_node_child_process30.spawnSync)("ip", ["-j", "-d", "-4", "address", "show", "dev", bridge], {
|
|
14136
15142
|
encoding: "utf8",
|
|
14137
15143
|
timeout: 5e3,
|
|
14138
15144
|
maxBuffer: 65536
|
|
@@ -14386,7 +15392,7 @@ function createManagedNetworkSession(input) {
|
|
|
14386
15392
|
"Insufficient endpoint headroom; request an allowed larger network before first allocation."
|
|
14387
15393
|
);
|
|
14388
15394
|
const authority = readNetworkOperationAuthority(identity2);
|
|
14389
|
-
const fingerprint = (0,
|
|
15395
|
+
const fingerprint = (0, import_node_crypto22.createHash)("sha256").update(input.plan().sourceConfigSha256).update(fingerprintComposeNetworkModel(model)).digest("hex");
|
|
14390
15396
|
if (!saved) {
|
|
14391
15397
|
if (policy.status !== "valid")
|
|
14392
15398
|
throw new Error("New network allocation requires valid machine policy.");
|
|
@@ -14888,7 +15894,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
14888
15894
|
target
|
|
14889
15895
|
});
|
|
14890
15896
|
if (target.kind === "linked") {
|
|
14891
|
-
const workspaceEnv = (0,
|
|
15897
|
+
const workspaceEnv = (0, import_node_child_process31.spawnSync)(
|
|
14892
15898
|
"docker",
|
|
14893
15899
|
["exec", appContainer.id, "printenv", "WORKSPACE"],
|
|
14894
15900
|
{ encoding: "utf-8", ...networkDockerOptions() }
|
|
@@ -14898,7 +15904,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
14898
15904
|
`Workspace app container must expose WORKSPACE='${target.workspace}' (got '${workspaceEnv.stdout.trim() || "(empty)"}').`
|
|
14899
15905
|
);
|
|
14900
15906
|
}
|
|
14901
|
-
const devrouterWorkspaceEnv = (0,
|
|
15907
|
+
const devrouterWorkspaceEnv = (0, import_node_child_process31.spawnSync)(
|
|
14902
15908
|
"docker",
|
|
14903
15909
|
["exec", appContainer.id, "printenv", "DEVROUTER_WORKSPACE"],
|
|
14904
15910
|
{ encoding: "utf-8", ...networkDockerOptions() }
|
|
@@ -14909,7 +15915,7 @@ async function waitForContainerPreflight(repoPath, target, upstreamHosts, timeou
|
|
|
14909
15915
|
);
|
|
14910
15916
|
}
|
|
14911
15917
|
}
|
|
14912
|
-
const gitCheck = (0,
|
|
15918
|
+
const gitCheck = (0, import_node_child_process31.spawnSync)(
|
|
14913
15919
|
"docker",
|
|
14914
15920
|
[
|
|
14915
15921
|
"exec",
|
|
@@ -15071,6 +16077,9 @@ function assertRepairBaseline(options) {
|
|
|
15071
16077
|
};
|
|
15072
16078
|
const reasons = [];
|
|
15073
16079
|
if (state.profile !== profile) reasons.push("profile");
|
|
16080
|
+
if ((state.variant ?? "default") !== (options.variant ?? "default")) reasons.push("variant");
|
|
16081
|
+
if (state.hostPortDeclarationsSha256 !== managedHostPortDeclarationsIdentity(config.managedRuntime?.devcontainer.hostPorts ?? {}))
|
|
16082
|
+
reasons.push("host-ports");
|
|
15074
16083
|
if (state.sourceConfigSha256 !== plan.sourceConfigSha256) reasons.push("source-config");
|
|
15075
16084
|
if (state.effectiveConfigSha256 !== plan.effectiveConfigSha256) reasons.push("effective-config");
|
|
15076
16085
|
for (const resource of ["apps", "services", "processes"]) {
|
|
@@ -15189,7 +16198,7 @@ function isPrimaryCheckout(repoPath) {
|
|
|
15189
16198
|
}
|
|
15190
16199
|
function openUrls(urls) {
|
|
15191
16200
|
for (const url of urls) {
|
|
15192
|
-
const opened = (0,
|
|
16201
|
+
const opened = (0, import_node_child_process31.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
15193
16202
|
if (opened.status !== 0) {
|
|
15194
16203
|
process.stderr.write(`Warning: could not open '${url}'.
|
|
15195
16204
|
`);
|
|
@@ -15260,6 +16269,12 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15260
16269
|
target.kind === "primary" ? "" : target.workspace,
|
|
15261
16270
|
options.profile ?? (options.repair ? previousManagedState?.profile : void 0)
|
|
15262
16271
|
);
|
|
16272
|
+
const variant = resolveRuntimeVariant(
|
|
16273
|
+
runtime.config,
|
|
16274
|
+
options.variant ?? (options.repair ? previousManagedState?.variant : void 0)
|
|
16275
|
+
);
|
|
16276
|
+
if (variant !== "default" && managedPostStart.kind !== "runtime")
|
|
16277
|
+
throw new Error("Runtime variants require a managed runtime post-start adapter.");
|
|
15263
16278
|
const managedRuntime = runtime.config.managedRuntime;
|
|
15264
16279
|
if (options.repair && !managedRuntime)
|
|
15265
16280
|
throw new Error("Repair requires managedRuntime configuration.");
|
|
@@ -15335,6 +16350,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15335
16350
|
repoPath,
|
|
15336
16351
|
...target.kind === "linked" ? { workspace: target.workspace } : {},
|
|
15337
16352
|
profile: runtime.profile,
|
|
16353
|
+
variant,
|
|
15338
16354
|
// A refusal precedes the provider start, so the exact id may not
|
|
15339
16355
|
// be known yet on a primary checkout.
|
|
15340
16356
|
devpodId: devpodId ?? "(absent)",
|
|
@@ -15364,6 +16380,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15364
16380
|
plan: managedPlan,
|
|
15365
16381
|
config: runtime.config,
|
|
15366
16382
|
profile: runtime.profile,
|
|
16383
|
+
variant,
|
|
15367
16384
|
processes: desiredProcesses,
|
|
15368
16385
|
routes: previousRoutes,
|
|
15369
16386
|
workspace: managedWorkspaceEnv
|
|
@@ -15472,7 +16489,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15472
16489
|
transitionPhase = "service-start";
|
|
15473
16490
|
reportLifecycleProgress("service-start");
|
|
15474
16491
|
claimLifecycleEffect();
|
|
15475
|
-
const result = (0,
|
|
16492
|
+
const result = (0, import_node_child_process31.spawnSync)("docker", ["start", ...stopped.map((entry) => entry.id)], {
|
|
15476
16493
|
...networkDockerOptions(),
|
|
15477
16494
|
encoding: "utf-8",
|
|
15478
16495
|
timeout: DEFAULT_READINESS_TIMEOUT_MS,
|
|
@@ -15582,6 +16599,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15582
16599
|
devpodId: captureId,
|
|
15583
16600
|
composeProject: captureProject,
|
|
15584
16601
|
profile: runtime.profile,
|
|
16602
|
+
variant,
|
|
16603
|
+
hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
|
|
16604
|
+
managedRuntime?.devcontainer.hostPorts ?? {}
|
|
16605
|
+
),
|
|
15585
16606
|
desired: {
|
|
15586
16607
|
apps: apps.map((app) => app.name).sort(),
|
|
15587
16608
|
services: [...capturePlan.desiredProfileServices].sort(),
|
|
@@ -15628,6 +16649,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15628
16649
|
devpodId,
|
|
15629
16650
|
composeProject: managedComposeProject,
|
|
15630
16651
|
profile: runtime.profile,
|
|
16652
|
+
variant,
|
|
16653
|
+
hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
|
|
16654
|
+
managedRuntime?.devcontainer.hostPorts ?? {}
|
|
16655
|
+
),
|
|
15631
16656
|
desired: {
|
|
15632
16657
|
apps: apps.map((app) => app.name).sort(),
|
|
15633
16658
|
services: [...managedPlan.desiredProfileServices].sort(),
|
|
@@ -15651,7 +16676,17 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15651
16676
|
container,
|
|
15652
16677
|
quiet: options.quiet,
|
|
15653
16678
|
profile: runtime.profile,
|
|
15654
|
-
|
|
16679
|
+
variant,
|
|
16680
|
+
...managedPlan ? {
|
|
16681
|
+
processes: desiredProcesses,
|
|
16682
|
+
hostPorts: resolveManagedHostPorts({
|
|
16683
|
+
repoPath,
|
|
16684
|
+
declarations: managedRuntime?.devcontainer.hostPorts ?? {},
|
|
16685
|
+
selectedServices: managedPlan.desiredServices,
|
|
16686
|
+
composeProject: managedComposeProject,
|
|
16687
|
+
composeFiles: managedPlan.composeFiles
|
|
16688
|
+
})
|
|
16689
|
+
} : {}
|
|
15655
16690
|
});
|
|
15656
16691
|
if (managedPlan && managedComposeProject) {
|
|
15657
16692
|
const processRegistry = runtime.config.managedRuntime?.processes ?? [];
|
|
@@ -15744,7 +16779,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15744
16779
|
plan: managedPostStart,
|
|
15745
16780
|
container: recoveredContainer,
|
|
15746
16781
|
quiet: options.quiet,
|
|
15747
|
-
profile: runtime.profile
|
|
16782
|
+
profile: runtime.profile,
|
|
16783
|
+
variant
|
|
15748
16784
|
});
|
|
15749
16785
|
recreated = true;
|
|
15750
16786
|
claimLifecycleEffect();
|
|
@@ -15776,6 +16812,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15776
16812
|
devpodId,
|
|
15777
16813
|
composeProject: managedComposeProject,
|
|
15778
16814
|
profile: runtime.profile,
|
|
16815
|
+
variant,
|
|
16816
|
+
hostPortDeclarationsSha256: managedHostPortDeclarationsIdentity(
|
|
16817
|
+
managedRuntime?.devcontainer.hostPorts ?? {}
|
|
16818
|
+
),
|
|
15779
16819
|
desired: {
|
|
15780
16820
|
apps: apps.map((app) => app.name).sort(),
|
|
15781
16821
|
services: [...managedPlan.desiredProfileServices].sort(),
|
|
@@ -15807,6 +16847,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15807
16847
|
workspace: target.workspace,
|
|
15808
16848
|
config: runtime.config,
|
|
15809
16849
|
profile: runtime.profile,
|
|
16850
|
+
variant,
|
|
15810
16851
|
resolvedProfile: runtime.resolvedProfile,
|
|
15811
16852
|
...options.repair ? { candidateState } : {}
|
|
15812
16853
|
}) : void 0;
|
|
@@ -15824,6 +16865,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
15824
16865
|
repoPath,
|
|
15825
16866
|
workspace: target.workspace,
|
|
15826
16867
|
profile: runtime.profile,
|
|
16868
|
+
variant,
|
|
15827
16869
|
devpodId,
|
|
15828
16870
|
urls,
|
|
15829
16871
|
recreated,
|
|
@@ -16016,13 +17058,32 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16016
17058
|
throw new Error("Managed post-start plan disappeared during rollback.");
|
|
16017
17059
|
}
|
|
16018
17060
|
if (previousProcesses.length > 0 && previousManagedState?.status !== "degraded") {
|
|
17061
|
+
if (previousManagedState?.hostPortDeclarationsSha256 !== managedHostPortDeclarationsIdentity(
|
|
17062
|
+
managedRuntimeConfig?.managedRuntime?.devcontainer.hostPorts ?? {}
|
|
17063
|
+
)) {
|
|
17064
|
+
throw new Error(
|
|
17065
|
+
"Host-port declarations changed; restore the retained declarations before repair."
|
|
17066
|
+
);
|
|
17067
|
+
}
|
|
16019
17068
|
claimLifecycleEffect();
|
|
16020
17069
|
runManagedPostStart({
|
|
16021
17070
|
plan: managedPostStartPlan,
|
|
16022
17071
|
container: managedContainer,
|
|
16023
17072
|
quiet: options.quiet,
|
|
16024
17073
|
profile: previousManagedState?.profile ?? "full",
|
|
16025
|
-
|
|
17074
|
+
variant: previousManagedState?.variant ?? "default",
|
|
17075
|
+
processes: previousProcesses,
|
|
17076
|
+
hostPorts: resolveManagedHostPorts({
|
|
17077
|
+
repoPath,
|
|
17078
|
+
declarations: managedRuntimeConfig?.managedRuntime?.devcontainer.hostPorts ?? {},
|
|
17079
|
+
selectedServices: [
|
|
17080
|
+
managedPlan.primaryService,
|
|
17081
|
+
...managedPlan.baseServices,
|
|
17082
|
+
...previousServices
|
|
17083
|
+
],
|
|
17084
|
+
composeProject: managedComposeProject,
|
|
17085
|
+
composeFiles: managedPlan.composeFiles
|
|
17086
|
+
})
|
|
16026
17087
|
});
|
|
16027
17088
|
}
|
|
16028
17089
|
for (const process2 of managedProcessRegistry) {
|
|
@@ -16172,7 +17233,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16172
17233
|
ensureNetworkLocked,
|
|
16173
17234
|
() => network?.endpoint
|
|
16174
17235
|
);
|
|
17236
|
+
preflightRuntimeVariant(repoPath, options);
|
|
16175
17237
|
return withLifecycleOperationLock(repoPath, async () => {
|
|
17238
|
+
preflightRuntimeVariant(repoPath, options);
|
|
16176
17239
|
const preparationConfig = loadRepoConfig(repoPath);
|
|
16177
17240
|
if (!options.repair)
|
|
16178
17241
|
recoverUnattachedNetworkReservation(repoPath, Boolean(preparationConfig.managedRuntime));
|
|
@@ -16213,7 +17276,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16213
17276
|
linked ? workspace : "",
|
|
16214
17277
|
requestedOptions.profile
|
|
16215
17278
|
).profile;
|
|
16216
|
-
|
|
17279
|
+
const desiredVariant = resolveRuntimeVariant(preparationConfig, requestedOptions.variant);
|
|
17280
|
+
if (desiredProfile !== retained.profile && desiredVariant === (retained.variant ?? "default")) {
|
|
16217
17281
|
const target = resolveRepairTarget(repoPath, linked);
|
|
16218
17282
|
const baseline = loadRuntimeConfig(repoPath, linked ? workspace : "", retained.profile);
|
|
16219
17283
|
const plan = inspectManagedDevcontainerConfig({
|
|
@@ -16231,6 +17295,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16231
17295
|
plan,
|
|
16232
17296
|
config: baseline.config,
|
|
16233
17297
|
profile: baseline.profile,
|
|
17298
|
+
variant: retained.variant ?? "default",
|
|
16234
17299
|
processes: desiredManagedProcesses(
|
|
16235
17300
|
baseline.config.managedRuntime,
|
|
16236
17301
|
baseline.resolvedProfile
|
|
@@ -16242,7 +17307,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16242
17307
|
claimLifecycleEffect();
|
|
16243
17308
|
return ensureLocked();
|
|
16244
17309
|
}
|
|
16245
|
-
options = {
|
|
17310
|
+
options = {
|
|
17311
|
+
...requestedOptions,
|
|
17312
|
+
repair: true,
|
|
17313
|
+
profile: retained.profile,
|
|
17314
|
+
variant: retained.variant ?? "default",
|
|
17315
|
+
open: false
|
|
17316
|
+
};
|
|
16246
17317
|
let repaired;
|
|
16247
17318
|
try {
|
|
16248
17319
|
repaired = await repairLocked();
|
|
@@ -16250,7 +17321,8 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
16250
17321
|
options = requestedOptions;
|
|
16251
17322
|
}
|
|
16252
17323
|
claimLifecycleEffect();
|
|
16253
|
-
if (desiredProfile !== repaired.profile
|
|
17324
|
+
if (desiredProfile !== repaired.profile || desiredVariant !== (repaired.variant ?? "default"))
|
|
17325
|
+
return ensureLocked();
|
|
16254
17326
|
if (options.open) openUrls(repaired.urls);
|
|
16255
17327
|
return repaired;
|
|
16256
17328
|
});
|
|
@@ -16336,6 +17408,33 @@ function reconcileDrained(record5) {
|
|
|
16336
17408
|
record5.worker = null;
|
|
16337
17409
|
}
|
|
16338
17410
|
}
|
|
17411
|
+
function preflightRuntimeVariant(repoPath, options) {
|
|
17412
|
+
if (options.variant === void 0 && !options.repair) return "default";
|
|
17413
|
+
const workspace = isLinkedWorktree(repoPath) ? readPersistedWorkspace(repoPath) : void 0;
|
|
17414
|
+
const retained = options.repair ? readManagedRuntimeState(repoPath, workspace) : void 0;
|
|
17415
|
+
const variant = resolveRuntimeVariant(
|
|
17416
|
+
loadRepoConfig(repoPath),
|
|
17417
|
+
options.variant ?? retained?.variant
|
|
17418
|
+
);
|
|
17419
|
+
if (options.repair && variant !== (retained?.variant ?? "default"))
|
|
17420
|
+
throw new Error("Repair requires the retained runtime variant.");
|
|
17421
|
+
if (variant !== "default") {
|
|
17422
|
+
if (resolveManagedPostStartPlan(repoPath).kind !== "runtime")
|
|
17423
|
+
throw new Error("Runtime variants require a managed runtime post-start adapter.");
|
|
17424
|
+
const previous = readReliabilityOperation({
|
|
17425
|
+
repoPath,
|
|
17426
|
+
workspace: workspace ?? null,
|
|
17427
|
+
provider: resolveWorkspaceRuntimeOrDefault(repoPath)
|
|
17428
|
+
});
|
|
17429
|
+
if (capacityPolicyTargets(repoPath) || previous?.state.executionPolicy === "capacity-managed")
|
|
17430
|
+
throw new Error("Non-default runtime variants require manual lifecycle admission.");
|
|
17431
|
+
}
|
|
17432
|
+
return variant;
|
|
17433
|
+
}
|
|
17434
|
+
function capacityPolicyTargets(repoPath) {
|
|
17435
|
+
const policy = readCapacityPolicy(import_node_path41.default.join(DEVROUTER_HOME, "controller"));
|
|
17436
|
+
return policy?.admissions === "enabled" && policy.enrollments.some((entry) => entry.repoPath === repoPath);
|
|
17437
|
+
}
|
|
16339
17438
|
function clearActiveLifecycleStartupWitness() {
|
|
16340
17439
|
const request = activeWorker;
|
|
16341
17440
|
if (!request || request.kind === "stop") return;
|
|
@@ -16393,12 +17492,46 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
16393
17492
|
request.repoPath,
|
|
16394
17493
|
() => proveInitialManagedDevsyAbsence(request.repoPath)
|
|
16395
17494
|
);
|
|
17495
|
+
const markStopWorkStarted = () => updateReliabilityOperation(request.identity, (record5) => {
|
|
17496
|
+
if (!matchesFence(record5, request.fence))
|
|
17497
|
+
throw new Error("Lifecycle intent changed before stop work began.");
|
|
17498
|
+
record5.stopWorkStarted = true;
|
|
17499
|
+
});
|
|
17500
|
+
if (request.options.repair && (!retained?.stopBaseline || request.options.delete || request.identity.provider !== "devsy"))
|
|
17501
|
+
throw new Error("Stop recovery requires a retained Devsy baseline and preserves data.");
|
|
16396
17502
|
if (retained?.stopBaseline) {
|
|
16397
|
-
stopBaselineState =
|
|
16398
|
-
withMutationLock(
|
|
17503
|
+
stopBaselineState = withMutationLock(
|
|
16399
17504
|
"Verify retained stop",
|
|
16400
17505
|
request.repoPath,
|
|
16401
|
-
() =>
|
|
17506
|
+
() => {
|
|
17507
|
+
let needsRecovery = false;
|
|
17508
|
+
if (request.options.repair) {
|
|
17509
|
+
try {
|
|
17510
|
+
proveManagedStop(retained);
|
|
17511
|
+
} catch {
|
|
17512
|
+
needsRecovery = true;
|
|
17513
|
+
}
|
|
17514
|
+
}
|
|
17515
|
+
const candidate = needsRecovery ? recoverManagedStopBaseline(
|
|
17516
|
+
retained,
|
|
17517
|
+
() => {
|
|
17518
|
+
const runtime = loadRuntimeConfig(
|
|
17519
|
+
request.repoPath,
|
|
17520
|
+
request.identity.workspace ?? "",
|
|
17521
|
+
retained.profile
|
|
17522
|
+
);
|
|
17523
|
+
return inspectManagedDevcontainerConfig({
|
|
17524
|
+
repoPath: request.repoPath,
|
|
17525
|
+
config: runtime.config,
|
|
17526
|
+
profile: runtime.resolvedProfile,
|
|
17527
|
+
linked: isLinkedWorktree(request.repoPath)
|
|
17528
|
+
});
|
|
17529
|
+
},
|
|
17530
|
+
markStopWorkStarted
|
|
17531
|
+
) : retained;
|
|
17532
|
+
proveManagedStop(candidate);
|
|
17533
|
+
return candidate;
|
|
17534
|
+
}
|
|
16402
17535
|
);
|
|
16403
17536
|
} else {
|
|
16404
17537
|
const containers = inspectWorkspaceContainers();
|
|
@@ -16416,11 +17549,7 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
16416
17549
|
];
|
|
16417
17550
|
for (const project of stopProjects) inspectManagedStopContainers(project);
|
|
16418
17551
|
}
|
|
16419
|
-
|
|
16420
|
-
if (!matchesFence(record5, request.fence))
|
|
16421
|
-
throw new Error("Lifecycle intent changed before stop work began.");
|
|
16422
|
-
record5.stopWorkStarted = true;
|
|
16423
|
-
});
|
|
17552
|
+
markStopWorkStarted();
|
|
16424
17553
|
} else {
|
|
16425
17554
|
updateReliabilityOperation(request.identity, (record5) => {
|
|
16426
17555
|
stepRecord(record5, {
|
|
@@ -16625,7 +17754,7 @@ async function devsyExecOutcome(repoPath, command, retainedProof) {
|
|
|
16625
17754
|
const launch = () => {
|
|
16626
17755
|
if (retainedProof) revalidateDevsyExecProof(repoPath, retainedProof);
|
|
16627
17756
|
claimLifecycleEffect();
|
|
16628
|
-
return (0,
|
|
17757
|
+
return (0, import_node_child_process32.spawn)("devsy", args, { stdio: "inherit" });
|
|
16629
17758
|
};
|
|
16630
17759
|
child = retainedProof ? withMutationLock("Execute retained workspace", repoPath, launch) : launch();
|
|
16631
17760
|
} catch (error) {
|
|
@@ -16673,7 +17802,7 @@ function ensureGuidance2(repoPath) {
|
|
|
16673
17802
|
return `Run 'devrouter ensure ${repoPath}' first.`;
|
|
16674
17803
|
}
|
|
16675
17804
|
function assertRunningDevpod(devpodId, repoPath) {
|
|
16676
|
-
const result = (0,
|
|
17805
|
+
const result = (0, import_node_child_process33.spawnSync)("devpod", ["status", devpodId, "--output", "json"], {
|
|
16677
17806
|
encoding: "utf-8"
|
|
16678
17807
|
});
|
|
16679
17808
|
if (result.status !== 0) {
|
|
@@ -16720,7 +17849,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
16720
17849
|
}
|
|
16721
17850
|
assertRunningDevpod(devpod.id, repoPath);
|
|
16722
17851
|
const workspaceDirectory = resolveWorkspaceDirectory(repoPath);
|
|
16723
|
-
const statusMarker = `__DEVROUTER_EXIT_${(0,
|
|
17852
|
+
const statusMarker = `__DEVROUTER_EXIT_${(0, import_node_crypto24.randomUUID)()}__:`;
|
|
16724
17853
|
const statusMarkerBytes = Buffer.from(statusMarker, "ascii");
|
|
16725
17854
|
const literalCommand = command.map(quotePosixArg).join(" ");
|
|
16726
17855
|
const wrappedCommand = `${literalCommand}; __devrouter_status=$?; printf '${statusMarker}%s\\n' "$__devrouter_status" >&2; exit 0`;
|
|
@@ -16852,7 +17981,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
16852
17981
|
let child;
|
|
16853
17982
|
try {
|
|
16854
17983
|
claimLifecycleEffect();
|
|
16855
|
-
child = (0,
|
|
17984
|
+
child = (0, import_node_child_process33.spawn)("devpod", args, { stdio: ["inherit", "inherit", "pipe"] });
|
|
16856
17985
|
} catch (error) {
|
|
16857
17986
|
settled = true;
|
|
16858
17987
|
reject(
|
|
@@ -16909,7 +18038,7 @@ async function devpodExecOutcome(repoPath, command) {
|
|
|
16909
18038
|
}
|
|
16910
18039
|
|
|
16911
18040
|
// src/core/workspace-lifecycle.ts
|
|
16912
|
-
var
|
|
18041
|
+
var import_node_child_process34 = require("child_process");
|
|
16913
18042
|
var import_node_fs37 = __toESM(require("fs"));
|
|
16914
18043
|
var import_node_path42 = __toESM(require("path"));
|
|
16915
18044
|
|