@timurproko/a1 0.1.1-dev.3 → 0.1.1-dev.5
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/bin/a1-ui.js +2 -2
- package/dist/src/composition/index.d.ts +0 -18
- package/dist/src/composition/index.js +0 -36
- package/dist/src/features/workspace/index.d.ts +0 -1
- package/dist/src/features/workspace/index.js +0 -1
- package/dist/src/features/workspace/router.d.ts +0 -5
- package/dist/src/features/workspace/router.js +0 -26
- package/dist/src/foundation/release/bootstrap.d.ts +2 -2
- package/dist/src/foundation/release/bootstrap.js +6 -6
- package/dist/src/foundation/release/release-store.d.ts +15 -1
- package/dist/src/foundation/release/release-store.js +53 -28
- package/dist/src/foundation/release/release.d.ts +15 -0
- package/dist/src/foundation/release/release.js +62 -35
- package/dist/src/foundation/release/update.d.ts +17 -1
- package/dist/src/foundation/release/update.js +51 -12
- package/docs/ci-release-runbook.md +63 -0
- package/package.json +10 -6
- package/dist/src/composition/structured-workspace-application.d.ts +0 -38
- package/dist/src/composition/structured-workspace-application.js +0 -261
- package/dist/src/features/workspace/structured-tabs.d.ts +0 -83
- package/dist/src/features/workspace/structured-tabs.js +0 -453
package/bin/a1-ui.js
CHANGED
|
@@ -4,11 +4,11 @@ const { runSelectedInteractiveRuntime } = await import("../dist/src/features/lau
|
|
|
4
4
|
|
|
5
5
|
runSelectedInteractiveRuntime(process.env.A1_LAUNCH_PROFILE ?? "a1", {
|
|
6
6
|
ownedUi: async () => {
|
|
7
|
-
const [{ runOwnedUi }, {
|
|
7
|
+
const [{ runOwnedUi }, { composeOwnedUiApplication }] = await Promise.all([
|
|
8
8
|
import("../dist/src/features/owned-ui/index.js"),
|
|
9
9
|
import("../dist/src/composition/index.js"),
|
|
10
10
|
]);
|
|
11
|
-
const application = await
|
|
11
|
+
const application = await composeOwnedUiApplication({ cwd: process.cwd() });
|
|
12
12
|
return await runOwnedUi({ application });
|
|
13
13
|
},
|
|
14
14
|
transparent: async profileId => {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { type AgentEnginePort } from "../foundation/agent-engine-contracts/index.js";
|
|
2
2
|
import { type PiEngineAdapter } from "../foundation/pi-engine-adapter/index.js";
|
|
3
3
|
import { type OwnedUiApplicationPort, type PresentationComponentPort, type PresentationRuntimePort, type PresentationTerminalPort } from "../foundation/presentation-contracts/index.js";
|
|
4
|
-
import { StructuredWorkspaceTabs, type StructuredWorkspaceLimits } from "../features/workspace/index.js";
|
|
5
|
-
import { StructuredWorkspaceApplication } from "./structured-workspace-application.js";
|
|
6
|
-
export * from "./structured-workspace-application.js";
|
|
7
4
|
export interface ProcessCompositionOptions {
|
|
8
5
|
readonly cwd?: string;
|
|
9
6
|
readonly sessionId?: string;
|
|
@@ -17,21 +14,6 @@ export interface OwnedUiCompositionOptions {
|
|
|
17
14
|
readonly createPiAdapter?: () => Promise<PiEngineAdapter>;
|
|
18
15
|
}
|
|
19
16
|
export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOptions): Promise<OwnedUiApplicationPort>;
|
|
20
|
-
export interface StructuredWorkspaceCompositionOptions {
|
|
21
|
-
readonly cwd?: string;
|
|
22
|
-
readonly workspaceId?: string;
|
|
23
|
-
readonly limits?: Partial<StructuredWorkspaceLimits>;
|
|
24
|
-
readonly createEngine?: (agentId: string) => Promise<AgentEnginePort>;
|
|
25
|
-
readonly createPiAdapter?: (agentId: string) => Promise<PiEngineAdapter>;
|
|
26
|
-
}
|
|
27
|
-
export declare function composeStructuredWorkspace(options?: StructuredWorkspaceCompositionOptions): StructuredWorkspaceTabs;
|
|
28
|
-
export interface StructuredWorkspaceApplicationCompositionOptions extends StructuredWorkspaceCompositionOptions {
|
|
29
|
-
readonly terminal?: PresentationTerminalPort;
|
|
30
|
-
readonly mode?: "regular" | "fullscreen";
|
|
31
|
-
readonly initialAgentId?: string;
|
|
32
|
-
readonly initialAgentName?: string;
|
|
33
|
-
}
|
|
34
|
-
export declare function composeStructuredWorkspaceApplication(options?: StructuredWorkspaceApplicationCompositionOptions): Promise<StructuredWorkspaceApplication>;
|
|
35
17
|
export interface ProcessComposition {
|
|
36
18
|
readonly engine: AgentEnginePort;
|
|
37
19
|
createPresentation(root: PresentationComponentPort, terminal: PresentationTerminalPort): PresentationRuntimePort;
|
|
@@ -3,9 +3,6 @@ import { createPiEngineAdapter } from "../foundation/pi-engine-adapter/index.js"
|
|
|
3
3
|
import { createPiPresentationRuntime, createPiTerminalBridge } from "../foundation/pi-tui-runtime-adapter/index.js";
|
|
4
4
|
import { assertPresentationComponent, assertPresentationRuntime, } from "../foundation/presentation-contracts/index.js";
|
|
5
5
|
import { OwnedUiSessionShell } from "../foundation/pi-owned-ui-integration/index.js";
|
|
6
|
-
import { StructuredWorkspaceTabs, } from "../features/workspace/index.js";
|
|
7
|
-
import { StructuredWorkspaceApplication } from "./structured-workspace-application.js";
|
|
8
|
-
export * from "./structured-workspace-application.js";
|
|
9
6
|
const CAPABILITIES = {
|
|
10
7
|
contractVersion: AGENT_ENGINE_CONTRACT_VERSION,
|
|
11
8
|
commands: ["prompt", "steer", "follow-up", "abort", "retry", "compact", "bash", "replace-session"],
|
|
@@ -28,39 +25,6 @@ export async function composeOwnedUiApplication(options = {}) {
|
|
|
28
25
|
dispose: () => shell.dispose(),
|
|
29
26
|
};
|
|
30
27
|
}
|
|
31
|
-
export function composeStructuredWorkspace(options = {}) {
|
|
32
|
-
const cwd = options.cwd ?? process.cwd();
|
|
33
|
-
const createEngine = options.createEngine ?? (async (agentId) => {
|
|
34
|
-
const sessionId = `${agentId}.session`;
|
|
35
|
-
const adapter = options.createPiAdapter
|
|
36
|
-
? await options.createPiAdapter(agentId)
|
|
37
|
-
: await createPiEngineAdapter({ cwd, sessionId });
|
|
38
|
-
return new PiAgentEngineBridge(adapter, cwd);
|
|
39
|
-
});
|
|
40
|
-
return new StructuredWorkspaceTabs({
|
|
41
|
-
cwd,
|
|
42
|
-
createEngine,
|
|
43
|
-
...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
|
|
44
|
-
...(options.limits === undefined ? {} : { limits: options.limits }),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
export async function composeStructuredWorkspaceApplication(options = {}) {
|
|
48
|
-
const workspace = composeStructuredWorkspace(options);
|
|
49
|
-
const created = await workspace.createAgent({
|
|
50
|
-
id: options.initialAgentId ?? "agent-1",
|
|
51
|
-
displayName: options.initialAgentName ?? "Agent 1",
|
|
52
|
-
});
|
|
53
|
-
if (created.kind === "rejected") {
|
|
54
|
-
await workspace.dispose();
|
|
55
|
-
throw new Error(`could not create the initial structured workspace agent: ${created.diagnostic}`);
|
|
56
|
-
}
|
|
57
|
-
return new StructuredWorkspaceApplication({
|
|
58
|
-
workspace,
|
|
59
|
-
cwd: options.cwd ?? process.cwd(),
|
|
60
|
-
...(options.terminal === undefined ? {} : { terminal: options.terminal }),
|
|
61
|
-
...(options.mode === undefined ? {} : { mode: options.mode }),
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
28
|
export async function composeProcess(options = {}) {
|
|
65
29
|
let engine = options.engine;
|
|
66
30
|
if (!engine) {
|
|
@@ -22,15 +22,10 @@ export declare class WorkspaceRouter {
|
|
|
22
22
|
constructor(reducer: WorkspaceReducer);
|
|
23
23
|
createAgent(agent: ManagedAgentDescriptor): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
24
24
|
selectAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
25
|
-
restartAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
26
|
-
markRecovered(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
27
|
-
markFailed(agentId: string, code: string, message: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
28
|
-
requestAttention(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
29
25
|
stopAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
30
26
|
removeAgent(agentId: string): Promise<WorkspaceRouterResult<WorkspaceAgentState>>;
|
|
31
27
|
recordActivity(agentId: string, amount?: number): Promise<WorkspaceRouterResult<number>>;
|
|
32
28
|
sendStructuredCommand(agentId: string, command: string, payload: unknown): Promise<WorkspaceRouterResult<RoutedStructuredCommand>>;
|
|
33
29
|
cancelStructuredCommand(agentId: string, targetCorrelationId: string): Promise<WorkspaceRouterResult<StructuredCommandRecord>>;
|
|
34
|
-
settleStructuredCommand(agentId: string, correlationId: string, outcome: "completed" | "failed"): Promise<WorkspaceRouterResult<StructuredCommandRecord>>;
|
|
35
30
|
view(): WorkspaceView;
|
|
36
31
|
}
|
|
@@ -23,18 +23,6 @@ export class WorkspaceRouter {
|
|
|
23
23
|
selectAgent(agentId) {
|
|
24
24
|
return this.#enqueue(() => this.reducer.selectAgent(agentId));
|
|
25
25
|
}
|
|
26
|
-
restartAgent(agentId) {
|
|
27
|
-
return this.#enqueue(() => this.reducer.restartAgent(agentId));
|
|
28
|
-
}
|
|
29
|
-
markRecovered(agentId) {
|
|
30
|
-
return this.#enqueue(() => this.reducer.markRecovered(agentId));
|
|
31
|
-
}
|
|
32
|
-
markFailed(agentId, code, message) {
|
|
33
|
-
return this.#enqueue(() => this.reducer.markFailed(agentId, code, message));
|
|
34
|
-
}
|
|
35
|
-
requestAttention(agentId) {
|
|
36
|
-
return this.#enqueue(() => this.reducer.requestAttention(agentId));
|
|
37
|
-
}
|
|
38
26
|
stopAgent(agentId) {
|
|
39
27
|
return this.#enqueue(() => this.reducer.stopAgent(agentId));
|
|
40
28
|
}
|
|
@@ -104,20 +92,6 @@ export class WorkspaceRouter {
|
|
|
104
92
|
return applied(this.reducer.view(), result.record);
|
|
105
93
|
});
|
|
106
94
|
}
|
|
107
|
-
settleStructuredCommand(agentId, correlationId, outcome) {
|
|
108
|
-
return this.#enqueue(() => {
|
|
109
|
-
const agent = this.reducer.view().agents.find(candidate => candidate.id === agentId);
|
|
110
|
-
if (!agent)
|
|
111
|
-
return reject("unknown-agent", `workspace agent does not exist: ${agentId}`);
|
|
112
|
-
const tracker = this.#trackerFor(agent);
|
|
113
|
-
if (!tracker)
|
|
114
|
-
return reject("capability-mismatch", `agent ${agent.id} is not structured`);
|
|
115
|
-
const result = tracker.complete(correlationId, outcome);
|
|
116
|
-
if (result.kind === "rejected")
|
|
117
|
-
return result;
|
|
118
|
-
return applied(this.reducer.view(), result.record);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
95
|
view() {
|
|
122
96
|
return this.reducer.view();
|
|
123
97
|
}
|
|
@@ -2,7 +2,7 @@ import { type OwnershipProbe } from "./cohort-selection.js";
|
|
|
2
2
|
import { type SupervisorEndpointMetadata } from "./cohort-state.js";
|
|
3
3
|
import { type LaunchProfileId } from "../lifecycle/index.js";
|
|
4
4
|
import { cleanupProvenIdleOwner } from "./process-cleanup.js";
|
|
5
|
-
import { type MaterializedRelease } from "./release-store.js";
|
|
5
|
+
import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
|
|
6
6
|
export interface BootstrapOptions {
|
|
7
7
|
readonly packageRoot: string;
|
|
8
8
|
readonly launchIntent?: {
|
|
@@ -15,7 +15,7 @@ export interface BootstrapOptions {
|
|
|
15
15
|
readonly output?: Pick<NodeJS.WriteStream, "write">;
|
|
16
16
|
}
|
|
17
17
|
export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
|
|
18
|
-
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string): Promise<string>;
|
|
18
|
+
export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
|
|
19
19
|
export declare function startSupervisor(release: MaterializedRelease, environment: NodeJS.ProcessEnv): Promise<void>;
|
|
20
20
|
export declare function releaseEnvironment(environment: NodeJS.ProcessEnv, release: MaterializedRelease): NodeJS.ProcessEnv;
|
|
21
21
|
export declare function waitForVerifiedEndpoint(path: string, release: MaterializedRelease, timeoutMs: number): Promise<void>;
|
|
@@ -8,7 +8,7 @@ import { CohortStateStore } from "./cohort-state.js";
|
|
|
8
8
|
import { assertLaunchProfileId, resolveProductPaths } from "../lifecycle/index.js";
|
|
9
9
|
import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
|
|
10
10
|
import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
|
|
11
|
-
import { materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
11
|
+
import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
|
|
12
12
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
13
13
|
export async function runBootstrap(options) {
|
|
14
14
|
const environment = { ...(options.environment ?? process.env) };
|
|
@@ -42,9 +42,7 @@ export async function runBootstrap(options) {
|
|
|
42
42
|
return await launchUi(retained, environment);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
const candidate = await materializeRelease(options.packageRoot, paths.dataDir
|
|
46
|
-
onProgress: progress => output.write(`${PRODUCT_TEXT.diagnostic(`installing ${progress.fileCount} files.`)}\n`),
|
|
47
|
-
});
|
|
45
|
+
const candidate = await materializeRelease(options.packageRoot, paths.dataDir);
|
|
48
46
|
await stateStore.recordCandidate(candidate);
|
|
49
47
|
state = await stateStore.read();
|
|
50
48
|
if (!state.references.active) {
|
|
@@ -125,8 +123,10 @@ async function readInstalledVersion(packageRoot) {
|
|
|
125
123
|
}
|
|
126
124
|
return manifest.version;
|
|
127
125
|
}
|
|
128
|
-
export async function certifyMaterializedRelease(release, dataDir) {
|
|
129
|
-
|
|
126
|
+
export async function certifyMaterializedRelease(release, dataDir, verification = {}) {
|
|
127
|
+
if (!consumeMaterializationProof(release)) {
|
|
128
|
+
await verifyMaterializedRelease(release.releaseRoot, release, resolve(dataDir, "releases"), verification);
|
|
129
|
+
}
|
|
130
130
|
const path = resolve(dataDir, `certification-${release.releaseId}.json`);
|
|
131
131
|
await writeFile(path, JSON.stringify({
|
|
132
132
|
schema: PRODUCT_IDENTITY.evidence.releaseCertificationSchema,
|
|
@@ -3,11 +3,23 @@ export declare const RELEASE_MANIFEST_FILENAME: string;
|
|
|
3
3
|
export interface MaterializedRelease extends ReleaseIdentity {
|
|
4
4
|
readonly releaseRoot: string;
|
|
5
5
|
}
|
|
6
|
+
export type ReleaseContentOperation = "source-read" | "candidate-write" | "verification-read";
|
|
7
|
+
export interface ReleaseContentOperationEvent {
|
|
8
|
+
readonly operation: ReleaseContentOperation;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly bytes: number;
|
|
11
|
+
}
|
|
6
12
|
export interface MaterializeReleaseOptions {
|
|
7
13
|
readonly onProgress?: (progress: {
|
|
8
14
|
readonly phase: "copying";
|
|
9
15
|
readonly fileCount: number;
|
|
10
16
|
}) => void;
|
|
17
|
+
readonly onOperation?: (event: ReleaseContentOperationEvent) => void;
|
|
18
|
+
/** Test seam for deterministic write-failure coverage. */
|
|
19
|
+
readonly writeCandidateFile?: (path: string, bytes: Uint8Array, mode: number) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface VerifyMaterializedReleaseOptions {
|
|
22
|
+
readonly onOperation?: (event: ReleaseContentOperationEvent) => void;
|
|
11
23
|
}
|
|
12
24
|
export interface CertifiedReleaseRecord {
|
|
13
25
|
readonly releaseId: string;
|
|
@@ -16,6 +28,8 @@ export interface CertifiedReleaseRecord {
|
|
|
16
28
|
readonly contentDigest: string;
|
|
17
29
|
}
|
|
18
30
|
export declare function materializeRelease(packageRoot: string, dataDir: string, options?: MaterializeReleaseOptions): Promise<MaterializedRelease>;
|
|
31
|
+
/** Consume proof that this exact object was freshly materialized or fully verified in this process. */
|
|
32
|
+
export declare function consumeMaterializationProof(release: MaterializedRelease): boolean;
|
|
19
33
|
export declare function readMaterializedRelease(releaseRoot: string): Promise<MaterializedRelease>;
|
|
20
34
|
/**
|
|
21
35
|
* Load metadata for a release whose bytes were already certified by the
|
|
@@ -24,6 +38,6 @@ export declare function readMaterializedRelease(releaseRoot: string): Promise<Ma
|
|
|
24
38
|
* verification.
|
|
25
39
|
*/
|
|
26
40
|
export declare function readCertifiedReleaseManifest(record: CertifiedReleaseRecord, selectedStoreRoot: string): Promise<MaterializedRelease>;
|
|
27
|
-
export declare function verifyMaterializedRelease(releaseRoot: string, expected?: ReleaseIdentity, selectedStoreRoot?: string): Promise<MaterializedRelease>;
|
|
41
|
+
export declare function verifyMaterializedRelease(releaseRoot: string, expected?: ReleaseIdentity, selectedStoreRoot?: string, options?: VerifyMaterializedReleaseOptions): Promise<MaterializedRelease>;
|
|
28
42
|
export declare function assertImmutableExecutionRoot(release: MaterializedRelease, dataDir: string): Promise<void>;
|
|
29
43
|
export declare function resolveReleaseEntryPoint(release: MaterializedRelease, entryPoint: string): Promise<string>;
|
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod,
|
|
2
|
+
import { chmod, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { PRODUCT_PACKAGE_NAME,
|
|
4
|
+
import { PRODUCT_PACKAGE_NAME, createReleaseIdentity, digestManifestFiles, discoverReleasePayload, releaseFileIdentity, resolveWithin, } from "./release.js";
|
|
5
5
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
6
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
7
7
|
const RELEASE_FILE_IO_CONCURRENCY = 32;
|
|
8
|
+
const certificationReady = new WeakSet();
|
|
8
9
|
export const RELEASE_MANIFEST_FILENAME = PRODUCT_IDENTITY.manifest.releaseFilename;
|
|
9
10
|
export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
10
|
-
const
|
|
11
|
+
const payload = await discoverReleasePayload(packageRoot, {
|
|
12
|
+
onSourceRead: (path, bytes) => options.onOperation?.({ operation: "source-read", path, bytes }),
|
|
13
|
+
});
|
|
11
14
|
const storeRoot = resolve(dataDir, "releases");
|
|
12
15
|
await mkdir(storeRoot, { recursive: true, mode: 0o700 });
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
if (existing)
|
|
16
|
-
return await verifyMaterializedRelease(releaseRoot, identity);
|
|
17
|
-
options.onProgress?.({ phase: "copying", fileCount: identity.files.length });
|
|
18
|
-
const candidate = resolveWithin(storeRoot, `.candidate-${identity.releaseId}-${randomUUID()}`);
|
|
16
|
+
options.onProgress?.({ phase: "copying", fileCount: payload.paths.length });
|
|
17
|
+
const candidate = resolveWithin(storeRoot, `.candidate-${randomUUID()}`);
|
|
19
18
|
await mkdir(candidate, { recursive: false, mode: 0o700 });
|
|
20
19
|
try {
|
|
21
|
-
const directories = [...new Set(
|
|
20
|
+
const directories = [...new Set(payload.paths.map(path => dirname(resolveWithin(candidate, path))))];
|
|
22
21
|
await mapWithConcurrency(directories, RELEASE_FILE_IO_CONCURRENCY, async (directory) => {
|
|
23
22
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
24
23
|
});
|
|
25
|
-
await mapWithConcurrency(
|
|
26
|
-
const source = resolveWithin(
|
|
27
|
-
const destination = resolveWithin(candidate,
|
|
28
|
-
await
|
|
29
|
-
|
|
24
|
+
const files = await mapWithConcurrency(payload.paths, RELEASE_FILE_IO_CONCURRENCY, async (path) => {
|
|
25
|
+
const source = resolveWithin(payload.packageRoot, path);
|
|
26
|
+
const destination = resolveWithin(candidate, path);
|
|
27
|
+
const metadata = await lstat(source);
|
|
28
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
29
|
+
throw new Error(`release payload is not a regular file: ${path}`);
|
|
30
|
+
const cached = payload.cachedFiles.get(path);
|
|
31
|
+
const bytes = cached ?? await readFile(source);
|
|
32
|
+
if (!cached)
|
|
33
|
+
options.onOperation?.({ operation: "source-read", path, bytes: bytes.length });
|
|
34
|
+
const mode = (metadata.mode & 0o111) !== 0 ? 0o500 : 0o400;
|
|
35
|
+
if (options.writeCandidateFile)
|
|
36
|
+
await options.writeCandidateFile(destination, bytes, mode);
|
|
37
|
+
else {
|
|
38
|
+
await writeFile(destination, bytes, { flag: "wx", mode });
|
|
39
|
+
await chmod(destination, mode);
|
|
40
|
+
}
|
|
41
|
+
options.onOperation?.({ operation: "candidate-write", path, bytes: bytes.length });
|
|
42
|
+
return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
|
|
30
43
|
});
|
|
44
|
+
const identity = createReleaseIdentity(payload.packageRoot, payload.packageVersion, files);
|
|
45
|
+
const releaseRoot = resolveWithin(storeRoot, identity.releaseId);
|
|
46
|
+
if (await lstat(releaseRoot).catch(() => null)) {
|
|
47
|
+
await rm(candidate, { recursive: true, force: true });
|
|
48
|
+
return certificationReadyRelease(await verifyMaterializedRelease(releaseRoot, identity));
|
|
49
|
+
}
|
|
31
50
|
await writeFile(resolve(candidate, RELEASE_MANIFEST_FILENAME), JSON.stringify(identity, null, 2), { mode: 0o400, flag: "wx" });
|
|
32
51
|
try {
|
|
33
52
|
await rename(candidate, releaseRoot);
|
|
@@ -36,15 +55,22 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
|
|
|
36
55
|
if (!await lstat(releaseRoot).catch(() => null))
|
|
37
56
|
throw error;
|
|
38
57
|
await rm(candidate, { recursive: true, force: true });
|
|
39
|
-
return await verifyMaterializedRelease(releaseRoot, identity);
|
|
58
|
+
return certificationReadyRelease(await verifyMaterializedRelease(releaseRoot, identity));
|
|
40
59
|
}
|
|
41
|
-
return { ...identity, releaseRoot: await realpath(releaseRoot) };
|
|
60
|
+
return certificationReadyRelease({ ...identity, releaseRoot: await realpath(releaseRoot) });
|
|
42
61
|
}
|
|
43
62
|
catch (error) {
|
|
44
63
|
await rm(candidate, { recursive: true, force: true });
|
|
45
64
|
throw error;
|
|
46
65
|
}
|
|
47
66
|
}
|
|
67
|
+
/** Consume proof that this exact object was freshly materialized or fully verified in this process. */
|
|
68
|
+
export function consumeMaterializationProof(release) {
|
|
69
|
+
if (!certificationReady.has(release))
|
|
70
|
+
return false;
|
|
71
|
+
certificationReady.delete(release);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
48
74
|
export async function readMaterializedRelease(releaseRoot) {
|
|
49
75
|
const canonical = await realpath(releaseRoot);
|
|
50
76
|
const manifest = JSON.parse(await readFile(resolve(canonical, RELEASE_MANIFEST_FILENAME), "utf8"));
|
|
@@ -69,7 +95,7 @@ export async function readCertifiedReleaseManifest(record, selectedStoreRoot) {
|
|
|
69
95
|
throw new Error(`release directory does not match identity ${manifest.releaseId}`);
|
|
70
96
|
return { ...manifest, releaseRoot: canonical };
|
|
71
97
|
}
|
|
72
|
-
export async function verifyMaterializedRelease(releaseRoot, expected, selectedStoreRoot) {
|
|
98
|
+
export async function verifyMaterializedRelease(releaseRoot, expected, selectedStoreRoot, options = {}) {
|
|
73
99
|
const canonical = await realpath(releaseRoot);
|
|
74
100
|
if (selectedStoreRoot)
|
|
75
101
|
assertContained(await realpath(selectedStoreRoot), canonical, "release root is outside the selected release store");
|
|
@@ -83,7 +109,7 @@ export async function verifyMaterializedRelease(releaseRoot, expected, selectedS
|
|
|
83
109
|
throw new Error(`release directory does not match identity ${manifest.releaseId}`);
|
|
84
110
|
}
|
|
85
111
|
await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
|
|
86
|
-
await verifyFile(canonical, file);
|
|
112
|
+
await verifyFile(canonical, file, options);
|
|
87
113
|
});
|
|
88
114
|
const recomputed = digestManifestFiles(manifest.files);
|
|
89
115
|
if (recomputed !== manifest.contentDigest)
|
|
@@ -109,17 +135,23 @@ export async function resolveReleaseEntryPoint(release, entryPoint) {
|
|
|
109
135
|
assertContained(release.releaseRoot, canonical, "entry point resolves outside the selected release root");
|
|
110
136
|
return canonical;
|
|
111
137
|
}
|
|
112
|
-
async function verifyFile(root, file) {
|
|
138
|
+
async function verifyFile(root, file, options) {
|
|
113
139
|
const path = resolveWithin(root, file.path);
|
|
114
140
|
const metadata = await lstat(path).catch(() => null);
|
|
115
141
|
if (!metadata?.isFile() || metadata.isSymbolicLink())
|
|
116
142
|
throw new Error(`release candidate is incomplete: ${file.path}`);
|
|
117
143
|
if (metadata.size !== file.bytes)
|
|
118
144
|
throw new Error(`release file size mismatch: ${file.path}`);
|
|
119
|
-
const
|
|
145
|
+
const bytes = await readFile(path);
|
|
146
|
+
options.onOperation?.({ operation: "verification-read", path: file.path, bytes: bytes.length });
|
|
147
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
120
148
|
if (digest !== file.sha256)
|
|
121
149
|
throw new Error(`release file digest mismatch: ${file.path}`);
|
|
122
150
|
}
|
|
151
|
+
function certificationReadyRelease(release) {
|
|
152
|
+
certificationReady.add(release);
|
|
153
|
+
return release;
|
|
154
|
+
}
|
|
123
155
|
function validateManifest(value) {
|
|
124
156
|
if (value.packageName !== PRODUCT_PACKAGE_NAME || typeof value.packageVersion !== "string")
|
|
125
157
|
throw new Error(PRODUCT_TEXT.diagnostic("release manifest metadata is invalid"));
|
|
@@ -135,13 +167,6 @@ function validateManifest(value) {
|
|
|
135
167
|
throw new Error(`invalid release manifest file identity: ${file.path}`);
|
|
136
168
|
}
|
|
137
169
|
}
|
|
138
|
-
function digestManifestFiles(files) {
|
|
139
|
-
const digest = createHash("sha256");
|
|
140
|
-
for (const file of [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) {
|
|
141
|
-
digest.update(`${file.path}\0${file.bytes}\0${file.sha256}\0${file.executable ? 1 : 0}\n`);
|
|
142
|
-
}
|
|
143
|
-
return digest.digest("hex");
|
|
144
|
-
}
|
|
145
170
|
function assertContained(parent, child, message) {
|
|
146
171
|
const fromParent = relative(parent, child);
|
|
147
172
|
if (fromParent === "" || (!fromParent.startsWith(`..${sep}`) && fromParent !== ".." && !isAbsolute(fromParent)))
|
|
@@ -13,11 +13,26 @@ export interface ReleaseIdentity {
|
|
|
13
13
|
readonly packageRoot: string;
|
|
14
14
|
readonly files: readonly ReleaseFileIdentity[];
|
|
15
15
|
}
|
|
16
|
+
export interface DiscoveredReleasePayload {
|
|
17
|
+
readonly packageRoot: string;
|
|
18
|
+
readonly packageVersion: string;
|
|
19
|
+
readonly paths: readonly string[];
|
|
20
|
+
/** Package manifests already read to discover the dependency closure. */
|
|
21
|
+
readonly cachedFiles: ReadonlyMap<string, Buffer>;
|
|
22
|
+
}
|
|
23
|
+
export interface DiscoverReleasePayloadOptions {
|
|
24
|
+
readonly onSourceRead?: (path: string, bytes: number) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Discover the complete payload without reading ordinary file contents. */
|
|
27
|
+
export declare function discoverReleasePayload(packageRoot: string, options?: DiscoverReleasePayloadOptions): Promise<DiscoveredReleasePayload>;
|
|
16
28
|
/**
|
|
17
29
|
* Derive release execution identity only from installed distribution metadata and
|
|
18
30
|
* bytes. The version remains display metadata; the digest selects executable
|
|
19
31
|
* content.
|
|
20
32
|
*/
|
|
21
33
|
export declare function deriveReleaseIdentity(packageRoot: string): Promise<ReleaseIdentity>;
|
|
34
|
+
export declare function createReleaseIdentity(packageRoot: string, packageVersion: string, files: readonly ReleaseFileIdentity[]): ReleaseIdentity;
|
|
35
|
+
export declare function releaseFileIdentity(path: string, bytes: Uint8Array, executable: boolean): ReleaseFileIdentity;
|
|
36
|
+
export declare function digestManifestFiles(files: readonly ReleaseFileIdentity[]): string;
|
|
22
37
|
export declare function resolveWithin(root: string, candidate: string): string;
|
|
23
38
|
export declare function packageRootFromModule(moduleUrl: string): string;
|
|
@@ -5,53 +5,78 @@ import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
|
5
5
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
6
6
|
const RELEASE_FILE_IO_CONCURRENCY = 32;
|
|
7
7
|
export const PRODUCT_PACKAGE_NAME = PRODUCT_IDENTITY.packageName;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
* bytes. The version remains display metadata; the digest selects executable
|
|
11
|
-
* content.
|
|
12
|
-
*/
|
|
13
|
-
export async function deriveReleaseIdentity(packageRoot) {
|
|
8
|
+
/** Discover the complete payload without reading ordinary file contents. */
|
|
9
|
+
export async function discoverReleasePayload(packageRoot, options = {}) {
|
|
14
10
|
const canonicalRoot = await realpath(packageRoot);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
11
|
+
const cachedFiles = new Map();
|
|
12
|
+
const readManifest = async (manifestRoot) => {
|
|
13
|
+
const manifestPath = resolve(manifestRoot, "package.json");
|
|
14
|
+
const bytes = await readFile(manifestPath);
|
|
15
|
+
const path = normalizeRelative(relative(canonicalRoot, manifestPath));
|
|
16
|
+
cachedFiles.set(path, bytes);
|
|
17
|
+
options.onSourceRead?.(path, bytes.length);
|
|
18
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
19
|
+
};
|
|
20
|
+
const manifest = await readManifest(canonicalRoot);
|
|
17
21
|
if (manifest.name !== PRODUCT_PACKAGE_NAME)
|
|
18
22
|
throw new Error(`unexpected ${PRODUCT_TEXT.displayName} package name: ${String(manifest.name)}`);
|
|
19
23
|
if (typeof manifest.version !== "string" || manifest.version.length === 0)
|
|
20
24
|
throw new Error(PRODUCT_TEXT.diagnostic("package metadata has no version"));
|
|
21
25
|
const roots = distributionRoots(manifest);
|
|
22
26
|
const paths = new Set(["package.json"]);
|
|
23
|
-
for (const root of roots)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
for (const root of roots)
|
|
28
|
+
await collectFiles(canonicalRoot, resolveWithin(canonicalRoot, root), paths);
|
|
29
|
+
await collectDependencyClosure(canonicalRoot, canonicalRoot, manifest, paths, new Set(), readManifest);
|
|
30
|
+
return {
|
|
31
|
+
packageRoot: canonicalRoot,
|
|
32
|
+
packageVersion: manifest.version,
|
|
33
|
+
paths: [...paths].sort(),
|
|
34
|
+
cachedFiles,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Derive release execution identity only from installed distribution metadata and
|
|
39
|
+
* bytes. The version remains display metadata; the digest selects executable
|
|
40
|
+
* content.
|
|
41
|
+
*/
|
|
42
|
+
export async function deriveReleaseIdentity(packageRoot) {
|
|
43
|
+
const payload = await discoverReleasePayload(packageRoot);
|
|
44
|
+
const files = await mapWithConcurrency(payload.paths, RELEASE_FILE_IO_CONCURRENCY, async (path) => {
|
|
45
|
+
const absolute = resolveWithin(payload.packageRoot, path);
|
|
31
46
|
const metadata = await lstat(absolute);
|
|
32
|
-
if (!metadata.isFile())
|
|
47
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
33
48
|
throw new Error(`release payload is not a regular file: ${path}`);
|
|
34
|
-
const bytes = await readFile(absolute);
|
|
35
|
-
return
|
|
36
|
-
path: normalizeRelative(path),
|
|
37
|
-
bytes: bytes.length,
|
|
38
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
39
|
-
executable: (metadata.mode & 0o111) !== 0,
|
|
40
|
-
};
|
|
49
|
+
const bytes = payload.cachedFiles.get(path) ?? await readFile(absolute);
|
|
50
|
+
return releaseFileIdentity(path, bytes, (metadata.mode & 0o111) !== 0);
|
|
41
51
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const contentDigest =
|
|
52
|
+
return createReleaseIdentity(payload.packageRoot, payload.packageVersion, files);
|
|
53
|
+
}
|
|
54
|
+
export function createReleaseIdentity(packageRoot, packageVersion, files) {
|
|
55
|
+
const contentDigest = digestManifestFiles(files);
|
|
46
56
|
return {
|
|
47
57
|
packageName: PRODUCT_PACKAGE_NAME,
|
|
48
|
-
packageVersion
|
|
58
|
+
packageVersion,
|
|
49
59
|
contentDigest,
|
|
50
|
-
releaseId: `${
|
|
51
|
-
packageRoot
|
|
52
|
-
files,
|
|
60
|
+
releaseId: `${packageVersion}-${contentDigest.slice(0, 20)}`,
|
|
61
|
+
packageRoot,
|
|
62
|
+
files: [...files].sort(compareReleaseFiles),
|
|
53
63
|
};
|
|
54
64
|
}
|
|
65
|
+
export function releaseFileIdentity(path, bytes, executable) {
|
|
66
|
+
return {
|
|
67
|
+
path: normalizeRelative(path),
|
|
68
|
+
bytes: bytes.length,
|
|
69
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
70
|
+
executable,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function digestManifestFiles(files) {
|
|
74
|
+
const digest = createHash("sha256");
|
|
75
|
+
for (const file of [...files].sort(compareReleaseFiles)) {
|
|
76
|
+
digest.update(`${file.path}\0${file.bytes}\0${file.sha256}\0${file.executable ? 1 : 0}\n`);
|
|
77
|
+
}
|
|
78
|
+
return digest.digest("hex");
|
|
79
|
+
}
|
|
55
80
|
export function resolveWithin(root, candidate) {
|
|
56
81
|
if (candidate.includes("\0"))
|
|
57
82
|
throw new Error("release path contains a null byte");
|
|
@@ -93,7 +118,7 @@ async function collectFiles(root, path, output, skipNodeModules = false) {
|
|
|
93
118
|
await collectFiles(root, resolve(path, entry), output, skipNodeModules);
|
|
94
119
|
}
|
|
95
120
|
}
|
|
96
|
-
async function collectDependencyClosure(root, requesterRoot, manifest, output, visited) {
|
|
121
|
+
async function collectDependencyClosure(root, requesterRoot, manifest, output, visited, readManifest) {
|
|
97
122
|
const required = dependencyNames(manifest.dependencies);
|
|
98
123
|
const optional = new Set(dependencyNames(manifest.optionalDependencies));
|
|
99
124
|
for (const name of [...new Set([...required, ...optional])].sort()) {
|
|
@@ -107,8 +132,7 @@ async function collectDependencyClosure(root, requesterRoot, manifest, output, v
|
|
|
107
132
|
continue;
|
|
108
133
|
visited.add(packagePath);
|
|
109
134
|
await collectFiles(root, packagePath, output, true);
|
|
110
|
-
|
|
111
|
-
await collectDependencyClosure(root, packagePath, dependencyManifest, output, visited);
|
|
135
|
+
await collectDependencyClosure(root, packagePath, await readManifest(packagePath), output, visited, readManifest);
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
async function findInstalledDependency(root, requesterRoot, name) {
|
|
@@ -136,6 +160,9 @@ function dependencyNames(value) {
|
|
|
136
160
|
throw new Error(PRODUCT_TEXT.diagnostic("dependency metadata is invalid"));
|
|
137
161
|
return Object.keys(value);
|
|
138
162
|
}
|
|
163
|
+
function compareReleaseFiles(left, right) {
|
|
164
|
+
return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
165
|
+
}
|
|
139
166
|
function normalizeRelative(path) {
|
|
140
167
|
return path.split(sep).join("/").replace(/^\.\//, "");
|
|
141
168
|
}
|
|
@@ -18,6 +18,18 @@ export interface UpdateOutput {
|
|
|
18
18
|
stdout(message: string): void;
|
|
19
19
|
stderr(message: string): void;
|
|
20
20
|
}
|
|
21
|
+
export type UpdateMeasuredPhase = "package-version" | "target-resolution" | "global-root" | "ownership-release" | "npm-install" | "materialized" | "certified" | "active-reference-committed" | "supervisor-verified" | "transaction-complete";
|
|
22
|
+
export interface UpdatePhaseTimingEvent {
|
|
23
|
+
readonly phase: UpdateMeasuredPhase;
|
|
24
|
+
readonly durationMs: number;
|
|
25
|
+
}
|
|
26
|
+
export interface UpdatePerformanceEvidence {
|
|
27
|
+
readonly fileCount: number;
|
|
28
|
+
readonly sourceReads: number;
|
|
29
|
+
readonly candidateWrites: number;
|
|
30
|
+
readonly verificationReads: number;
|
|
31
|
+
readonly postNpmDurationMs: number;
|
|
32
|
+
}
|
|
21
33
|
export interface SelfUpdateOptions {
|
|
22
34
|
packageRoot: string;
|
|
23
35
|
channel?: UpdateChannel;
|
|
@@ -27,14 +39,17 @@ export interface SelfUpdateOptions {
|
|
|
27
39
|
runner?: UpdateProcessRunner;
|
|
28
40
|
lifecycle?: UpdateLifecycleCoordinator;
|
|
29
41
|
transactionStore?: UpdateTransactionJournal;
|
|
42
|
+
onPhaseTiming?: (event: UpdatePhaseTimingEvent) => void;
|
|
43
|
+
now?: () => number;
|
|
30
44
|
}
|
|
45
|
+
export type UpdateActivationPhase = Extract<UpdateTransactionPhase, "materialized" | "certified" | "active-reference-committed">;
|
|
31
46
|
export interface UpdateLifecycleCoordinator {
|
|
32
47
|
targetIsActive(targetVersion: string): Promise<boolean>;
|
|
33
48
|
shutdownVerifiedOwners(targetVersion: string): Promise<{
|
|
34
49
|
priorActiveVersion: string | null;
|
|
35
50
|
}>;
|
|
36
51
|
verifyPackageUnlocked(packageRoot: string): Promise<void>;
|
|
37
|
-
activateInstalled(packageRoot: string, targetVersion: string, phase: (phase:
|
|
52
|
+
activateInstalled(packageRoot: string, targetVersion: string, phase: (phase: UpdateActivationPhase) => Promise<void>): Promise<void>;
|
|
38
53
|
}
|
|
39
54
|
export interface UpdateTransactionJournal {
|
|
40
55
|
readonly path: string;
|
|
@@ -52,3 +67,4 @@ export interface UpdateTransactionJournal {
|
|
|
52
67
|
export declare function createNpmProcessRunner(platform?: NodeJS.Platform): UpdateProcessRunner;
|
|
53
68
|
export declare function createUpdateLifecycleCoordinator(environment?: NodeJS.ProcessEnv, fileSystem?: UpdateFileSystem, output?: UpdateOutput): UpdateLifecycleCoordinator;
|
|
54
69
|
export declare function runSelfUpdate(options: SelfUpdateOptions): Promise<number>;
|
|
70
|
+
export declare function assertUpdatePerformanceBudget(evidence: UpdatePerformanceEvidence, maximumPostNpmDurationMs?: number): void;
|