Skip to content
This repository was archived by the owner on Oct 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/coordination-readers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# CLI coordination readers
# Legacy CLI coordination readers (migration compatibility)

An operator can connect the existing host reader interface when starting DevSpace:
**C4 migration boundary:** New `cutover_start` and single-conversation or managed-worktree `dependency_sync` use DevSpace-native durable operations, not Carrier/ControlPlane admission. Ordinary startup does not initialize the Carrier store unless a historical persisted bound effect still needs recovery, or an operator explicitly supplies legacy completion/coordination readers. The commands below document historical compatibility, not a supported path for introducing new governance. Do not use them to start new cutovers or direct dependency operations.

An operator recovering a historical bound effect can connect its original trusted host reader interface when starting DevSpace:

```sh
devspace serve --coordination-reader-module /absolute/path/readers.mjs --coordination-reader-sha256 <64-hex-sha256>
Expand Down Expand Up @@ -49,10 +51,11 @@ Pairing remains available. Selecting the module pins executable code, not eviden
truth: the reviewed reader must still validate its canonical sources, artifact
integrity and acceptance provenance. Synthetic fixtures cannot establish delivery.

Without a custom reader module, the server now exposes a local pairing path for
bounded dependency operations. Unpaired sessions have no coordination authority.
An explicitly configured custom reader retains exclusive control; the built-in
pairing tools are not enabled alongside it.
The following pairing workflow describes **historical persisted Carrier sessions**.
It is no longer the ordinary MCP execution path, and `coordination_pair` /
`coordination_resume` are not registered as new direct-operation tools.
A custom legacy reader remains solely responsible for the original authority
of a pre-existing bound record; it cannot authorize a new direct operation.

1. The MCP client calls `coordination_pair` and keeps its private credential.
The pending ID identifies the pairing request; it does not grant authority.
Expand Down
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ function printHelp(): void {
" devspace config set publicBaseUrl <url|null>",
" devspace models List available models in catalog",
" devspace models refresh Refresh current provider model catalog and bump generation",
"",
"Legacy Carrier administration (persisted bound recovery only; not needed for new direct execution):",
" devspace carrier inspect <pending-id>",
" devspace carrier show <carrier-id>",
" devspace carrier reauthorize <carrier-id> --validity-version <version> --until <ISO expiry>",
Expand Down
29 changes: 29 additions & 0 deletions src/cutover-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,35 @@ for (const missing of ["witnessCutoverId", "witnessServerInstanceId", "witnessEx

const coordinationBinding = {leaseId:"lease-one",pinnedLeaseVersion:2,operationHandle:"operation-one",requestHash:"a".repeat(64),ownerThread:"controller-one"};

test("C4 direct correlation cannot be malformed, mixed with Carrier, or changed during drain", () => {
const root = mkdtempSync(join(tmpdir(), "devspace-c4-direct-correlation-"));
try {
const store = new CutoverStateStore(root);
const directOperation = { operationId: "op_" + "a".repeat(16), requestHash: "b".repeat(64) };
const base = { oldServerIdentity: oldIdentity, expectedNewIdentity: { sourceCommit: "target", buildId: "target" } };
assert.throws(() => store.begin({ ...base, coordinationBinding, directOperation }), /cannot mix/i);
assert.throws(() => store.begin({ ...base, directOperation: { ...directOperation, requestHash: "bad" } }), /malformed direct/i);
assert.deepEqual(readdirSync(root), []);
const created = store.begin({ ...base, directOperation });
assert.equal(created.coordinationBinding, undefined);
assert.deepEqual(new CutoverStateStore(root).get()?.directOperation, directOperation);
store.recordDrain(created.cutoverId, { activeSessions: 0, oldestAgeMs: 0 });
const active = join(root, "cutover", "active");
const drain = join(active, readdirSync(active).find(name => name.startsWith("drained-"))!);
const original = readFileSync(drain, "utf8");
for (const tamper of [undefined, { ...directOperation, requestHash: "c".repeat(64) }]) {
const changed = JSON.parse(original);
if (tamper) changed.directOperation = tamper; else delete changed.directOperation;
writeFileSync(drain, JSON.stringify(changed));
assert.throws(() => new CutoverStateStore(root).get(), /correlation changed/i);
writeFileSync(drain, original);
}
assert.deepEqual(new CutoverStateStore(root).get()?.directOperation, directOperation);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("coordination binding is validated before writes and survives restart and drain", () => {
const root=mkdtempSync(join(tmpdir(),"devspace-cutover-correlation-"));
try {
Expand Down
31 changes: 29 additions & 2 deletions src/cutover-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,17 @@ export interface CutoverCoordinationBinding {
readonly ownerThread: string;
}

/** Correlation only: an OWNER_DIRECT cutover does not grant carrier authority. */
export interface CutoverDirectOperationBinding {
readonly operationId: string;
readonly requestHash: string;
}

export interface DurableCutoverRecord {
/** Trusted consumer correlation only; never issues authority. */
coordinationBinding?: Readonly<CutoverCoordinationBinding>;
/** Exact durable intent of a new OWNER_DIRECT generation; not an authority grant. */
directOperation?: Readonly<CutoverDirectOperationBinding>;
schema: typeof CUTOVER_STATE_SCHEMA;
cutoverId: string;
phase: "prepared" | "drained" | "closed" | "superseded";
Expand Down Expand Up @@ -484,15 +492,19 @@ export class CutoverStateStore {
expectedNewIdentity: ExpectedCutoverIdentity;
expiresAt?: string;
coordinationBinding?: CutoverCoordinationBinding;
directOperation?: CutoverDirectOperationBinding;
}): DurableCutoverRecord {
const coordinationBinding = input.coordinationBinding === undefined ? undefined : validatedCoordinationBinding(input.coordinationBinding);
const directOperation = input.directOperation === undefined ? undefined : validatedDirectOperationBinding(input.directOperation);
if (coordinationBinding && directOperation) throw new CutoverStateError("Cutover cannot mix Carrier authority with OWNER_DIRECT intent.");
mkdirSync(this.cutoverRoot, { recursive: true, mode: 0o700 });
const now = new Date(this.now()).toISOString();
const record: DurableCutoverRecord = {
schema: CUTOVER_STATE_SCHEMA,
cutoverId: this.newId(),
phase: "prepared",
...(coordinationBinding ? { coordinationBinding } : {}),
...(directOperation ? { directOperation } : {}),
oldServerIdentity: input.oldServerIdentity,
expectedNewIdentity: input.expectedNewIdentity,
createdAt: now,
Expand Down Expand Up @@ -1627,9 +1639,22 @@ function validatedCoordinationBinding(value: unknown): Readonly<CutoverCoordinat
return Object.freeze({...binding}) as unknown as Readonly<CutoverCoordinationBinding>;
}

function validatedDirectOperationBinding(value: unknown): Readonly<CutoverDirectOperationBinding> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new CutoverStateError("Malformed direct cutover operation binding.");
const binding = value as Record<string, unknown>;
if (Object.keys(binding).length !== 2 ||
!/^op_[a-f0-9]{16}$/.test(String(binding.operationId)) ||
!/^[a-f0-9]{64}$/.test(String(binding.requestHash))) {
throw new CutoverStateError("Malformed direct cutover operation binding.");
}
return Object.freeze({operationId: binding.operationId as string, requestHash: binding.requestHash as string});
}

function assertSameCoordinationBinding(created: DurableCutoverRecord, event: DurableCutoverRecord): void {
if (created.cutoverId !== event.cutoverId || !isDeepStrictEqual(created.coordinationBinding, event.coordinationBinding)) {
throw new CutoverStateError("Cutover coordination binding changed within its generation.");
if (created.cutoverId !== event.cutoverId ||
!isDeepStrictEqual(created.coordinationBinding, event.coordinationBinding) ||
!isDeepStrictEqual(created.directOperation, event.directOperation)) {
throw new CutoverStateError("Cutover coordination binding or direct operation correlation changed within its generation.");
}
}

Expand Down Expand Up @@ -1663,6 +1688,8 @@ function parseRecord(raw: string): DurableCutoverRecord {
}
const record = value as DurableCutoverRecord;
if (record.coordinationBinding !== undefined) record.coordinationBinding = validatedCoordinationBinding(record.coordinationBinding);
if (record.directOperation !== undefined) record.directOperation = validatedDirectOperationBinding(record.directOperation);
if (record.coordinationBinding && record.directOperation) throw new CutoverStateError("Cutover cannot mix Carrier authority with OWNER_DIRECT intent.");
if (record.bindingRepair) {
assertValidBindingRepair(record.bindingRepair, record);
}
Expand Down
Loading
Loading