rampway 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +31 -0
  2. package/build/src/velocious/auth.d.ts +17 -0
  3. package/build/src/velocious/auth.d.ts.map +1 -0
  4. package/build/src/velocious/auth.js +30 -0
  5. package/build/src/velocious/auth.js.map +1 -0
  6. package/build/src/velocious/control-plane.d.ts +177 -0
  7. package/build/src/velocious/control-plane.d.ts.map +1 -0
  8. package/build/src/velocious/control-plane.js +290 -0
  9. package/build/src/velocious/control-plane.js.map +1 -0
  10. package/build/src/velocious/controller.d.ts +58 -0
  11. package/build/src/velocious/controller.d.ts.map +1 -0
  12. package/build/src/velocious/controller.js +143 -0
  13. package/build/src/velocious/controller.js.map +1 -0
  14. package/build/src/velocious/executor.d.ts +55 -0
  15. package/build/src/velocious/executor.d.ts.map +1 -0
  16. package/build/src/velocious/executor.js +142 -0
  17. package/build/src/velocious/executor.js.map +1 -0
  18. package/build/src/velocious/index.d.ts +31 -0
  19. package/build/src/velocious/index.d.ts.map +1 -0
  20. package/build/src/velocious/index.js +144 -0
  21. package/build/src/velocious/index.js.map +1 -0
  22. package/build/src/velocious/path-matcher.d.ts +18 -0
  23. package/build/src/velocious/path-matcher.d.ts.map +1 -0
  24. package/build/src/velocious/path-matcher.js +48 -0
  25. package/build/src/velocious/path-matcher.js.map +1 -0
  26. package/build/src/velocious/registry.d.ts +25 -0
  27. package/build/src/velocious/registry.d.ts.map +1 -0
  28. package/build/src/velocious/registry.js +48 -0
  29. package/build/src/velocious/registry.js.map +1 -0
  30. package/build/src/velocious/run-store.d.ts +347 -0
  31. package/build/src/velocious/run-store.d.ts.map +1 -0
  32. package/build/src/velocious/run-store.js +569 -0
  33. package/build/src/velocious/run-store.js.map +1 -0
  34. package/build/src/velocious/sanitize.d.ts +23 -0
  35. package/build/src/velocious/sanitize.d.ts.map +1 -0
  36. package/build/src/velocious/sanitize.js +154 -0
  37. package/build/src/velocious/sanitize.js.map +1 -0
  38. package/build/src/velocious/worker.d.ts +48 -0
  39. package/build/src/velocious/worker.d.ts.map +1 -0
  40. package/build/src/velocious/worker.js +165 -0
  41. package/build/src/velocious/worker.js.map +1 -0
  42. package/docs/docker-development-environment.md +103 -0
  43. package/docs/velocious-deployment-api.md +226 -0
  44. package/package.json +22 -3
@@ -0,0 +1,154 @@
1
+ // @ts-check
2
+ const MAX_STRING_LENGTH = 4000;
3
+ const MAX_DEPTH = 20;
4
+ const MAX_COLLECTION_ENTRIES = 100;
5
+ const MAX_VISITED_VALUES = 1000;
6
+ const MAX_SERIALIZED_BYTES = 64 * 1024;
7
+ const MAX_PREVIEW_LENGTH = 8000;
8
+ const REDACTED = "[redacted]";
9
+ const TRUNCATION_MARKER = "[truncated]";
10
+ const TRUNCATED_VALUE = Symbol("truncated");
11
+ /** @typedef {{visited: number}} SanitizerState */
12
+ /**
13
+ * @param {string} value - Value to redact.
14
+ * @param {string[]} secrets - Exact configured secrets.
15
+ * @returns {string} Redacted and bounded value.
16
+ */
17
+ function redactString(value, secrets) {
18
+ let result = value;
19
+ for (const secret of secrets) {
20
+ if (secret.length > 0)
21
+ result = result.split(secret).join(REDACTED);
22
+ }
23
+ return result.length > MAX_STRING_LENGTH ? `${result.slice(0, MAX_STRING_LENGTH)}…` : result;
24
+ }
25
+ /**
26
+ * Converts an integration value to bounded JSON data while redacting secrets
27
+ * in values and keys. Colliding redacted keys are retained with stable suffixes.
28
+ *
29
+ * @param {unknown} value - Integration value.
30
+ * @param {string[]} secrets - Secrets to remove.
31
+ * @param {number} [depth] - Current depth.
32
+ * @returns {any} JSON-safe value or undefined.
33
+ */
34
+ export function sanitizeAdapterValue(value, secrets, depth = 0) {
35
+ const sanitized = sanitizeValue(value, secrets, depth, { visited: 0 });
36
+ if (sanitized === undefined)
37
+ return undefined;
38
+ if (sanitized === TRUNCATED_VALUE)
39
+ return { reason: "Sanitized output exceeded traversal limits", truncated: true };
40
+ const serialized = JSON.stringify(sanitized);
41
+ if (Buffer.byteLength(serialized) <= MAX_SERIALIZED_BYTES)
42
+ return sanitized;
43
+ return {
44
+ preview: serialized.slice(0, MAX_PREVIEW_LENGTH),
45
+ reason: `Sanitized output exceeded ${MAX_SERIALIZED_BYTES} bytes`,
46
+ truncated: true
47
+ };
48
+ }
49
+ /**
50
+ * @param {unknown} value - Integration value.
51
+ * @param {string[]} secrets - Secrets to remove.
52
+ * @param {number} depth - Current depth.
53
+ * @param {SanitizerState} state - Shared traversal budget.
54
+ * @returns {any} JSON-safe value, undefined, or the truncation sentinel.
55
+ */
56
+ function sanitizeValue(value, secrets, depth, state) {
57
+ if (state.visited >= MAX_VISITED_VALUES || depth > MAX_DEPTH)
58
+ return TRUNCATED_VALUE;
59
+ state.visited++;
60
+ if (value === null)
61
+ return null;
62
+ if (typeof value === "string")
63
+ return redactString(value, secrets);
64
+ if (typeof value === "number")
65
+ return Number.isFinite(value) ? value : null;
66
+ if (typeof value === "boolean")
67
+ return value;
68
+ if (Array.isArray(value)) {
69
+ const result = [];
70
+ let truncated = value.length > MAX_COLLECTION_ENTRIES;
71
+ const contentLimit = truncated ? MAX_COLLECTION_ENTRIES - 1 : value.length;
72
+ for (let index = 0; index < contentLimit; index++) {
73
+ const sanitized = sanitizeValue(value[index], secrets, depth + 1, state);
74
+ if (sanitized === TRUNCATED_VALUE) {
75
+ truncated = true;
76
+ break;
77
+ }
78
+ if (sanitized !== undefined)
79
+ result.push(sanitized);
80
+ }
81
+ if (truncated)
82
+ result.push(TRUNCATION_MARKER);
83
+ return result;
84
+ }
85
+ if (typeof value !== "object")
86
+ return undefined;
87
+ const prototype = Object.getPrototypeOf(value);
88
+ if (prototype !== Object.prototype && prototype !== null)
89
+ return undefined;
90
+ /** @type {Record<string, any>} */
91
+ const result = {};
92
+ const keys = [];
93
+ let truncated = false;
94
+ for (const key in value) {
95
+ if (!Object.hasOwn(value, key))
96
+ continue;
97
+ keys.push(key);
98
+ if (keys.length > MAX_COLLECTION_ENTRIES) {
99
+ truncated = true;
100
+ keys.length = MAX_COLLECTION_ENTRIES - 1;
101
+ break;
102
+ }
103
+ }
104
+ for (const key of keys) {
105
+ const sanitized = sanitizeValue(/** @type {Record<string, unknown>} */ (value)[key], secrets, depth + 1, state);
106
+ if (sanitized === TRUNCATED_VALUE) {
107
+ truncated = true;
108
+ break;
109
+ }
110
+ if (sanitized === undefined)
111
+ continue;
112
+ defineAvailableProperty(result, redactString(key, secrets), sanitized);
113
+ }
114
+ if (truncated)
115
+ defineAvailableProperty(result, TRUNCATION_MARKER, true);
116
+ return result;
117
+ }
118
+ /**
119
+ * Defines a property without dropping a colliding diagnostic key.
120
+ *
121
+ * @param {Record<string, any>} target - Sanitized object.
122
+ * @param {string} key - Desired key.
123
+ * @param {unknown} value - Sanitized value.
124
+ * @returns {void}
125
+ */
126
+ function defineAvailableProperty(target, key, value) {
127
+ let availableKey = key;
128
+ let collision = 2;
129
+ while (Object.hasOwn(target, availableKey)) {
130
+ availableKey = `${key}#${collision}`;
131
+ collision++;
132
+ }
133
+ Object.defineProperty(target, availableKey, {
134
+ configurable: true,
135
+ enumerable: true,
136
+ value,
137
+ writable: true
138
+ });
139
+ }
140
+ /**
141
+ * @param {unknown} error - Deployment failure.
142
+ * @param {string[]} secrets - Secrets to remove.
143
+ * @returns {{message: string, recovery: any}} Safe failure payload.
144
+ */
145
+ export function sanitizeErrorPayload(error, secrets) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ const recovery = error instanceof Error ? /** @type {Error & {recovery?: unknown}} */ (error).recovery : undefined;
148
+ const payload = sanitizeAdapterValue({ message, recovery }, secrets);
149
+ if (payload && typeof payload === "object" && typeof payload.message === "string") {
150
+ return { message: payload.message, recovery: payload.recovery };
151
+ }
152
+ return { message: "Deployment failure diagnostics were truncated", recovery: payload };
153
+ }
154
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../../src/velocious/sanitize.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @typedef {object} WorkerContext
3
+ * @property {any} [configuration] - Reconstructed Velocious configuration.
4
+ * @property {(args: {mountIdentifier: string, runId: string}) => Promise<void>} [execute] - Test-only bootstrap execution hook.
5
+ */
6
+ /**
7
+ * Parses the package-owned worker's bounded invocation contract.
8
+ *
9
+ * @param {string[]} argv - Worker arguments.
10
+ * @returns {{bootstrapPath: string, mountIdentifier: string, runId: string}} Invocation.
11
+ */
12
+ export function parseWorkerArguments(argv: string[]): {
13
+ bootstrapPath: string;
14
+ mountIdentifier: string;
15
+ runId: string;
16
+ };
17
+ /**
18
+ * Executes one admitted deployment in a fresh application process.
19
+ *
20
+ * @param {{bootstrapPath: string, mountIdentifier: string, runId: string}} args - Worker invocation.
21
+ * @returns {Promise<void>}
22
+ */
23
+ export function runDetachedDeploymentWorker({ bootstrapPath, mountIdentifier, runId }: {
24
+ bootstrapPath: string;
25
+ mountIdentifier: string;
26
+ runId: string;
27
+ }): Promise<void>;
28
+ /**
29
+ * @param {any} configuration - Configuration.
30
+ * @param {string} context - Error context.
31
+ * @param {unknown} error - Error.
32
+ * @returns {void}
33
+ */
34
+ export function reportWorkerFrameworkError(configuration: any, context: string, error: unknown): void;
35
+ export type WorkerContext = {
36
+ /**
37
+ * - Reconstructed Velocious configuration.
38
+ */
39
+ configuration?: any;
40
+ /**
41
+ * - Test-only bootstrap execution hook.
42
+ */
43
+ execute?: ((args: {
44
+ mountIdentifier: string;
45
+ runId: string;
46
+ }) => Promise<void>) | undefined;
47
+ };
48
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/velocious/worker.js"],"names":[],"mappings":"AAgBA;;;;GAIG;AAEH;;;;;GAKG;AACH,2CAHW,MAAM,EAAE,GACN;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,CAU3E;AAED;;;;;GAKG;AACH,uFAHW;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,GAC7D,OAAO,CAAC,IAAI,CAAC,CAsEzB;AA+BD;;;;;GAKG;AACH,0DALW,GAAG,WACH,MAAM,SACN,OAAO,GACL,IAAI,CAQhB;;;;;oBAzIa,GAAG;;;;sBACI;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC"}
@@ -0,0 +1,165 @@
1
+ // @ts-check
2
+ import { pathToFileURL } from "node:url";
3
+ import AppRoutes from "velocious/build/src/routes/app-routes.js";
4
+ import configurationResolver from "velocious/build/src/configuration-resolver.js";
5
+ import { DeploymentControlPlane, lookupTarget } from "./control-plane.js";
6
+ import { createRampwayDeploymentExecutor } from "./executor.js";
7
+ import { validateWorkerBootstrapPath } from "./controller.js";
8
+ import DeploymentRunStore from "./run-store.js";
9
+ import { getDeploymentMountByIdentifier } from "./registry.js";
10
+ import { sanitizeErrorPayload } from "./sanitize.js";
11
+ const MAX_IDENTIFIER_LENGTH = 128;
12
+ /**
13
+ * @typedef {object} WorkerContext
14
+ * @property {any} [configuration] - Reconstructed Velocious configuration.
15
+ * @property {(args: {mountIdentifier: string, runId: string}) => Promise<void>} [execute] - Test-only bootstrap execution hook.
16
+ */
17
+ /**
18
+ * Parses the package-owned worker's bounded invocation contract.
19
+ *
20
+ * @param {string[]} argv - Worker arguments.
21
+ * @returns {{bootstrapPath: string, mountIdentifier: string, runId: string}} Invocation.
22
+ */
23
+ export function parseWorkerArguments(argv) {
24
+ if (argv.length !== 3)
25
+ throw new Error("Rampway deployment worker requires bootstrap, mount, and run identifiers");
26
+ const [bootstrapPath, mountIdentifier, runId] = argv;
27
+ if (typeof mountIdentifier !== "string" || !/^[0-9a-f]{64}$/.test(mountIdentifier))
28
+ throw new Error("Invalid Rampway deployment worker mount identifier");
29
+ if (typeof runId !== "string" || runId.length === 0 || runId.length > MAX_IDENTIFIER_LENGTH || !/^[A-Za-z0-9_-]+$/.test(runId)) {
30
+ throw new Error("Invalid Rampway deployment worker run identifier");
31
+ }
32
+ return { bootstrapPath: validateWorkerBootstrapPath(bootstrapPath), mountIdentifier, runId };
33
+ }
34
+ /**
35
+ * Executes one admitted deployment in a fresh application process.
36
+ *
37
+ * @param {{bootstrapPath: string, mountIdentifier: string, runId: string}} args - Worker invocation.
38
+ * @returns {Promise<void>}
39
+ */
40
+ export async function runDetachedDeploymentWorker({ bootstrapPath, mountIdentifier, runId }) {
41
+ /** @type {any} */
42
+ let configuration;
43
+ /** @type {DeploymentRunStore|null} */
44
+ let store = null;
45
+ /** @type {import('./control-plane.js').DeploymentMountOptions & {databaseIdentifier: string, executor: import('./executor.js').RampwayDeploymentExecutor}|null} */
46
+ let options = null;
47
+ let claimed = null;
48
+ try {
49
+ const imported = await import(pathToFileURL(bootstrapPath).href);
50
+ const bootstrap = imported.default ?? imported.createRampwayDeploymentWorkerContext;
51
+ if (typeof bootstrap !== "function")
52
+ throw new Error("Rampway worker bootstrap must default-export a context factory");
53
+ const context = /** @type {WorkerContext} */ (await bootstrap({ mountIdentifier, runId }));
54
+ if (!context || typeof context !== "object")
55
+ throw new Error("Rampway worker bootstrap must return a context object");
56
+ if (typeof context.execute === "function") {
57
+ await context.execute({ mountIdentifier, runId });
58
+ return;
59
+ }
60
+ configuration = context.configuration;
61
+ if (!configuration)
62
+ throw new Error("Rampway worker bootstrap did not return a Velocious configuration");
63
+ await initializeWorkerConfiguration(configuration);
64
+ options = deploymentWorkerOptions(configuration, mountIdentifier, bootstrapPath);
65
+ store = createWorkerStore(configuration, options, mountIdentifier);
66
+ claimed = await store.claimPendingRun({ id: runId, startedAtMs: Date.now() });
67
+ const target = lookupTarget(options.projects, claimed.project, claimed.stage);
68
+ if (!target)
69
+ throw new Error(`Deployment run ${runId} no longer has an allowlisted target`);
70
+ const plane = new DeploymentControlPlane({
71
+ detachedExecution: async () => { throw new Error("Detached workers cannot launch nested deployment workers"); },
72
+ emitFrameworkError(error) {
73
+ reportWorkerFrameworkError(configuration, error.context, error.error);
74
+ },
75
+ executor: options.executor ?? createRampwayDeploymentExecutor(),
76
+ options,
77
+ store
78
+ });
79
+ await plane._executeRun({ claimed: true, run: claimed, target });
80
+ }
81
+ catch (error) {
82
+ if (!store || !options) {
83
+ try {
84
+ const resolveConfiguration = /** @type {(args: {directory: string}) => Promise<any>} */ (configurationResolver);
85
+ configuration ??= await resolveConfiguration({ directory: process.cwd() });
86
+ await initializeWorkerConfiguration(configuration);
87
+ options = deploymentWorkerOptions(configuration, mountIdentifier, bootstrapPath);
88
+ store = createWorkerStore(configuration, options, mountIdentifier);
89
+ }
90
+ catch (recoveryError) {
91
+ reportWorkerFrameworkError(configuration, "rampway-deployment-api-worker-recovery", recoveryError);
92
+ throw new AggregateError([error, recoveryError], "Rampway worker could not reconstruct its durable failure context");
93
+ }
94
+ }
95
+ const safeError = sanitizeErrorPayload(error, options.accessTokens);
96
+ try {
97
+ if (claimed) {
98
+ await store.markFailed({ error: safeError, finishedAtMs: Date.now(), id: claimed.id, ownerToken: claimed.ownerToken ?? "" });
99
+ }
100
+ else {
101
+ await store.failPendingRun({ error: safeError, finishedAtMs: Date.now(), id: runId });
102
+ }
103
+ await store.addAuditEvent({ event: "run_failed", payload: { message: safeError.message, phase: claimed ? "worker_execution" : "worker_bootstrap" }, runId });
104
+ }
105
+ catch (persistenceError) {
106
+ reportWorkerFrameworkError(configuration, "rampway-deployment-api-worker-failure-persistence", persistenceError);
107
+ throw new AggregateError([error, persistenceError], "Rampway worker failure could not be persisted");
108
+ }
109
+ return;
110
+ }
111
+ finally {
112
+ if (configuration && typeof configuration.closeDatabaseConnections === "function")
113
+ await configuration.closeDatabaseConnections();
114
+ }
115
+ }
116
+ /** @param {any} configuration - Configuration. @returns {Promise<void>} */
117
+ async function initializeWorkerConfiguration(configuration) {
118
+ const routes = await AppRoutes.getRoutes(configuration);
119
+ await configuration.initialize({ type: "rampway-deployment-worker" });
120
+ configuration.setRoutes(routes);
121
+ }
122
+ /**
123
+ * @param {any} configuration - Configuration.
124
+ * @param {string} mountIdentifier - Mount identifier.
125
+ * @param {string} bootstrapPath - Canonical bootstrap path.
126
+ * @returns {import('./control-plane.js').DeploymentMountOptions & {databaseIdentifier: string, executor: import('./executor.js').RampwayDeploymentExecutor}} Options.
127
+ */
128
+ function deploymentWorkerOptions(configuration, mountIdentifier, bootstrapPath) {
129
+ const options = /** @type {import('./control-plane.js').DeploymentMountOptions & {databaseIdentifier: string, executor: import('./executor.js').RampwayDeploymentExecutor}} */ (getDeploymentMountByIdentifier(configuration, mountIdentifier));
130
+ if (!options || options.workerBootstrapPath !== bootstrapPath)
131
+ throw new Error("Rampway deployment worker mount was not reconstructed");
132
+ return options;
133
+ }
134
+ /**
135
+ * @param {any} configuration - Configuration.
136
+ * @param {import('./control-plane.js').DeploymentMountOptions & {databaseIdentifier: string}} options - Mount options.
137
+ * @param {string} mountIdentifier - Mount identifier.
138
+ * @returns {DeploymentRunStore} Store.
139
+ */
140
+ function createWorkerStore(configuration, options, mountIdentifier) {
141
+ return new DeploymentRunStore({ configuration, databaseIdentifier: options.databaseIdentifier, mountIdentifier, staleRunTimeoutMs: options.staleRunTimeoutMs });
142
+ }
143
+ /**
144
+ * @param {any} configuration - Configuration.
145
+ * @param {string} context - Error context.
146
+ * @param {unknown} error - Error.
147
+ * @returns {void}
148
+ */
149
+ export function reportWorkerFrameworkError(configuration, context, error) {
150
+ const events = configuration?.getErrorEvents?.();
151
+ if (!events)
152
+ return;
153
+ const payload = { context, error };
154
+ events.emit("framework-error", payload);
155
+ events.emit("all-error", { ...payload, errorType: "framework-error" });
156
+ }
157
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
158
+ try {
159
+ await runDetachedDeploymentWorker(parseWorkerArguments(process.argv.slice(2)));
160
+ }
161
+ catch (_error) {
162
+ process.exitCode = 1;
163
+ }
164
+ }
165
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src/velocious/worker.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ # Docker development environment
2
+
3
+ Rampway has one canonical root development image and one persistent Compose service. The image is digest-pinned Ubuntu 26.04 LTS with exact NodeSource Node 24, a checksummed signing key, the universal coding/debugging baseline, and the current OpenCode, Codex, Claude Code, and official Kimi Code CLIs. The build is source-independent: `.dockerignore` closes the build context to the Dockerfile metadata, the image copies no repository content, and project dependencies are installed only inside the running service.
4
+
5
+ ## First use
6
+
7
+ Prerequisites are Docker with Compose v2 and a dedicated UID/GID-1000 development home containing this checkout at `$DEV_HOME_PATH/rampway`.
8
+
9
+ Copy the portable template and confirm its required read-only credential sources:
10
+
11
+ ```bash
12
+ cp .env.example .env
13
+ test -d "${GH_CONFIG_SOURCE_PATH:-$HOME/.config/gh}"
14
+ test -f "${CODEX_AUTH_SOURCE_PATH:-$HOME/.codex/auth.json}"
15
+ ```
16
+
17
+ Set `DEV_HOME_PATH` in `.env` to a dedicated home path containing this checkout at `$DEV_HOME_PATH/rampway`. Do not use an unrelated general host home or recursively change its ownership in the container. The environment template defaults to `/home/dev`; adjust it for any non-default host layout.
18
+
19
+ Run the preparation helper to create the required mount-target parent directories with correct permissions, then start the persistent service and install locked dependencies:
20
+
21
+ ```bash
22
+ scripts/prepare-dev-home.sh
23
+ docker compose up --build --detach dev
24
+ scripts/docker-run.sh npm ci
25
+ ```
26
+
27
+ `scripts/docker-run.sh` uses `docker compose exec -T dev` and preserves every command argument exactly. The service must already be running. Dependencies, caches, the checkout, and ordinary tool state persist naturally under the complete `/home/dev` bind.
28
+
29
+ ## Isolation and mounts
30
+
31
+ Compose declares the default project name `rampway`. Override it with `COMPOSE_PROJECT_NAME` (or `-p`) and use a distinct `DEV_HOME_PATH` for each concurrent instance. Run `scripts/prepare-dev-home.sh` to create required mount-target parent directories before the first start:
32
+
33
+ ```bash
34
+ COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
35
+ scripts/prepare-dev-home.sh
36
+ COMPOSE_PROJECT_NAME=rampway-review DEV_HOME_PATH=/srv/rampway-dev-homes/review \
37
+ docker compose up --build --detach dev
38
+ ```
39
+
40
+ The `dev` service has exactly these mounts:
41
+
42
+ - `${DEV_HOME_PATH:?Set DEV_HOME_PATH in .env}` at `/home/dev`;
43
+ - `${GH_CONFIG_SOURCE_PATH:?Set GH_CONFIG_SOURCE_PATH in .env}` at `/home/dev/.config/gh`, read-only;
44
+ - `${CODEX_AUTH_SOURCE_PATH:?Set CODEX_AUTH_SOURCE_PATH in .env}` at `/home/dev/.codex/auth.json`, read-only.
45
+
46
+ The GitHub config directory is selected through `GH_CONFIG_DIR`. Only the narrow Codex auth file is authorized; SSH keys, Docker sockets, broad host auth mounts, and separate Claude/Kimi/OpenCode secret paths are not. The tracked topology also forbids source split mounts, named source/session volumes, fixed names/ports, privileged mode, sidecars, and container-side ownership fixups.
47
+
48
+ Provider CLI installation is not provider authorization. Threadwire remains parent-owned and is resolved outside this service through bare unversioned `npx threadwire`; Rampway installs no global or project Threadwire package and tracks no provider workspace profile.
49
+
50
+ ## Static validation
51
+
52
+ The dependency-free verifier checks the image pin, NodeSource verification, provider specs, complete apt baseline, non-root identity, exact mounts, fail-closed interpolation, closed build context, forbidden topology, ignored `.env`, and helper contract. Its focused tests also inject representative regressions:
53
+
54
+ ```bash
55
+ npm run verify:docker-dev-environment
56
+ npm run test:docker-dev-environment
57
+ ```
58
+
59
+ ## Docker-capable coordinator validation
60
+
61
+ The bootstrap service has no Docker access. A coordinator must render Compose and build the image cleanly:
62
+
63
+ ```bash
64
+ export COMPOSE_PROJECT_NAME=rampway-canonical-validation
65
+ docker compose config --quiet
66
+ docker compose config
67
+ docker compose build --pull --no-cache dev
68
+ docker compose up --detach dev
69
+ docker compose exec -T dev sh -lc 'test "$HOME" = /home/dev && test "$PWD" = /home/dev/rampway && test "$(id -u)" = 1000 && test "$(id -g)" = 1000'
70
+ docker compose exec -T dev npm ci
71
+ docker compose exec -T dev npm run verify:docker-dev-environment
72
+ ```
73
+
74
+ To prove two-instance isolation, prepare two UID/GID-1000 development homes with Rampway at `<home>/rampway`, start both, and compare their container IDs:
75
+
76
+ ```bash
77
+ COMPOSE_PROJECT_NAME=rampway-isolation-a DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-a docker compose up --build --detach dev
78
+ COMPOSE_PROJECT_NAME=rampway-isolation-b DEV_HOME_PATH=/srv/rampway-dev-homes/isolation-b docker compose up --build --detach dev
79
+ test "$(COMPOSE_PROJECT_NAME=rampway-isolation-a docker compose ps -q dev)" != "$(COMPOSE_PROJECT_NAME=rampway-isolation-b docker compose ps -q dev)"
80
+ ```
81
+
82
+ For fresh cutover, provision a new dedicated home with Rampway already checked out at `<home>/rampway`, then perform a clean image build and dependency install:
83
+
84
+ ```bash
85
+ export RAMPWAY_COMPOSE_PROJECT=rampway-cutover-fresh
86
+ export RAMPWAY_DEV_HOME=/srv/rampway-dev-homes/cutover-fresh
87
+ test -d "$RAMPWAY_DEV_HOME/rampway/.git"
88
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose build --pull --no-cache dev
89
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
90
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T dev npm ci
91
+ ```
92
+
93
+ For exact resume, reuse the retained home and project name without reinstalling or relocating state, then prove the same checkout revision is visible:
94
+
95
+ ```bash
96
+ test -n "$RAMPWAY_COMPOSE_PROJECT"
97
+ test -d "$RAMPWAY_DEV_HOME/rampway/.git"
98
+ export RAMPWAY_EXPECTED_HEAD="$(git -C "$RAMPWAY_DEV_HOME/rampway" rev-parse HEAD)"
99
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose up --build --detach dev
100
+ COMPOSE_PROJECT_NAME="$RAMPWAY_COMPOSE_PROJECT" DEV_HOME_PATH="$RAMPWAY_DEV_HOME" docker compose exec -T -e RAMPWAY_EXPECTED_HEAD="$RAMPWAY_EXPECTED_HEAD" dev sh -lc 'test "$PWD" = /home/dev/rampway && test "$(git rev-parse HEAD)" = "$RAMPWAY_EXPECTED_HEAD"'
101
+ ```
102
+
103
+ The obsolete `/workspace` and named-volume state is not imported. Verify the new service before stopping the old project; removal of old named volumes is a separate destructive coordinator decision.