From 367e44810519ddc77480e988f7db7a9a631702b5 Mon Sep 17 00:00:00 2001 From: liying <422107224@qq.com> Date: Tue, 13 Oct 2026 04:41:51 +0800 Subject: [PATCH 1/2] Release v0.5.12 file activity through target-owned updates --- adapters/claude/README.md | 33 +- adapters/claude/package.json | 4 +- adapters/claude/src/claudeArchive.ts | 51 +- adapters/claude/src/claudeBackground.test.ts | 4 +- .../src/claudeBackgroundCompatibility.test.ts | 4 +- adapters/claude/src/claudeCapture.test.ts | 76 +- .../src/claudeGenericCompaction.test.ts | 2 +- adapters/codebuddy/package.json | 2 +- adapters/codex/README.md | 24 +- adapters/codex/package.json | 4 +- adapters/codex/src/codexArchive.test.ts | 225 ++- adapters/codex/src/codexArchive.ts | 90 +- adapters/codex/src/index.ts | 8 +- adapters/cursor/package.json | 2 +- adapters/grok/package.json | 2 +- adapters/kimi/package.json | 2 +- adapters/opencode/package.json | 2 +- apps/cli/package.json | 4 +- .../scripts/active-bridge-release-gate.mjs | 82 +- .../active-bridge-release-gate.node.mjs | 77 +- apps/cli/scripts/codex-clock-release-gate.mjs | 16 +- .../scripts/codex-clock-release-gate.node.mjs | 13 + .../scripts/verify-active-bridge-upgrade.mjs | 64 +- .../verify-codex-clock-upgrade-terminal.py | 3 +- .../scripts/verify-codex-clock-upgrade.mjs | 239 ++- apps/cli/scripts/verify-release.mjs | 25 +- apps/web/package.json | 2 +- compose.yaml | 10 +- deploy/server.Dockerfile | 4 +- deploy/web.Dockerfile | 4 +- docs/adapters/claude.md | 97 +- docs/adapters/codex.md | 131 +- docs/adapters/package-manifest.md | 22 +- docs/cli/setup-and-adapters.md | 48 +- docs/releases/evidence/v0.5.11-delivery.json | 80 + docs/releases/evidence/v0.5.12-staging.json | 66 + .../evidence/v0.5.12-update-acceptance.json | 1390 +++++++++++++++++ docs/releases/v0.5.12.md | 125 ++ docs/releasing.md | 35 + docs/session-analytics.md | 30 +- package.json | 2 +- packages/application/src/cliUpgrade.test.ts | 79 +- packages/application/src/cliUpgrade.ts | 9 +- packages/application/src/toolUpdates.test.ts | 51 +- packages/application/src/toolUpdates.ts | 2 +- server/internal/releaseinfo/release.go | 4 +- specs/auth-v1-release-gates.json | 10 +- specs/auth-v1-release.json | 6 +- 48 files changed, 2976 insertions(+), 289 deletions(-) create mode 100644 docs/releases/evidence/v0.5.11-delivery.json create mode 100644 docs/releases/evidence/v0.5.12-staging.json create mode 100644 docs/releases/evidence/v0.5.12-update-acceptance.json create mode 100644 docs/releases/v0.5.12.md diff --git a/adapters/claude/README.md b/adapters/claude/README.md index 01fece4..c5a4742 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -1,12 +1,16 @@ # Claude Code Adapter - -The 0.5.11 `legacy-bridge` release retains the published 0.5.10 capture -Implementation while the CLI gains target-owned automatic updates. -The Claude Adapter keeps Canonical/publication v4 and source profile -`claude.jsonl.active-path.2`. Ordinary native-tool identity and file-activity capture are -planned for the subsequent `target-owned` release. See +The 0.5.12 `target-owned` Adapter restores native tool identity and recorded +file activity through Canonical/publication v5, source profile +`claude.jsonl.active-path.4` and projection revision 7. The 0.5.11 +`legacy-bridge` retains its genuine v4/active-path.2 capture for older clients. +The verified target CLI owns preparation of this v5 Adapter; see [ADR-0126](../../docs/architecture/adr/0126-target-owned-managed-updates.md). +See the [0.5.12 release notes](../../docs/releases/v0.5.12.md) for publication, +deployment and acceptance evidence. + +Capture Project-scoped Claude Code JSONL through ATape's normal Collector, +shared ACP profile, Server and conversation reader. ```sh pnpm --filter @atape/adapter-claude build @@ -32,8 +36,8 @@ of the earlier implementation. The factory exposes only `atape.source-capture.v2`, with explicit `atape.legacy-migration.v1` support for acknowledged earlier checkpoints. A compatible -Host and publication-enabled Server with v4 targets and legacy adoption are required. -The Host must explicitly advertise Canonical v4 when opening a source; older +Host and publication-enabled Server with v5 targets and legacy adoption are required. +The Host must explicitly advertise Canonical v5 when opening a source; older Hosts are rejected before source reads instead of dropping normalized identities. This checkout requirement is separate from any published Adapter version. @@ -50,10 +54,21 @@ captured child can retain prior stored history with a diagnostic while the root advances. Raw-enabled capture also archives available historical child sources proved by admitted root receipts outside the selected path. -Explicit MCP and `Skill` tool invocations preserve bounded identities for +Explicit native tool names, MCP and `Skill` tool invocations preserve bounded identities for [Session Analytics](../../docs/session-analytics.md). Skill file reads and command expansions are not counted as Skill tools. The Server does not infer identity from labels or Raw, and upload redaction can explicitly clear a sensitive identity. +The effective source profile is `claude.jsonl.active-path.4`; existing v1 source +proof checkpoints remain readable. The Host replaces available older Canonical +projections once and retains existing Raw receipts. Replay, new identities and file activity +require available source bytes; a lost source does not erase captured history. + +The v5 projection adds explicit file activity: `Read` +`file_path`, `Write`/`Edit`/`MultiEdit` `file_path`, and `Grep`/`Glob` `path` scopes. +Paths retain their original spelling; searches do not claim matching files. +Missing, malformed, oversized or hidden paths remain unknown. Shell commands, +MCP inputs and search patterns are not parsed for paths. Upload redaction clears +sensitive copied facts. See the [Adapter guide](../../docs/adapters/claude.md#explicit-file-activity). Discovery uses `~/.claude/projects/*/*.jsonl`. `ATAPE_CLAUDE_HOME` selects an absolute alternate configuration directory; `ATAPE_CLAUDE_SESSION_FILE` selects an absolute diff --git a/adapters/claude/package.json b/adapters/claude/package.json index be0f5f1..61d14d3 100644 --- a/adapters/claude/package.json +++ b/adapters/claude/package.json @@ -1,6 +1,6 @@ { "name": "@atape/adapter-claude", - "version": "0.5.11", + "version": "0.5.12", "description": "ATape Adapter for project-scoped Claude Code local sessions", "license": "MIT", "type": "module", @@ -50,7 +50,7 @@ "gitAttribution": "atape.git-attribution.v1", "rawCapturePolicy": "atape.raw-capture.v1", "sourceCapture": "atape.source-capture.v2", - "publicationTargetProfile": "atape.publication-target.v4", + "publicationTargetProfile": "atape.publication-target.v5", "legacyMigration": "atape.legacy-migration.v1", "adapterId": "claude", "displayName": "Claude Code", diff --git a/adapters/claude/src/claudeArchive.ts b/adapters/claude/src/claudeArchive.ts index 53ac940..5717bad 100644 --- a/adapters/claude/src/claudeArchive.ts +++ b/adapters/claude/src/claudeArchive.ts @@ -1,7 +1,7 @@ import type { AcpSessionUpdate, AdapterEvent, AdapterOpenContext, AdapterUsage, SourceCaptureFrame, SourceCaptureLimits, SourceDiscoveryPage, SourceOpenRequestV2, SourceCaptureViewV2, SourceLegacyMigrationRequest } from "@atape/domain" import { Effect, Schema } from "effect" -import { AcpToolIdentity, CanonicalProfileVersion4, GitAttributionVersion, isBoundedToolValue, MaxSourceFailures, ToolIdentityMetaKey, type AdapterSourceFailure } from "@atape/domain" +import { AcpFileActivity, AcpToolIdentity, CanonicalProfileVersion5, FileActivityMetaKey, GitAttributionVersion, isBoundedToolValue, MaxSourceFailures, ToolIdentityMetaKey, type AdapterSourceFailure } from "@atape/domain" import { createHash } from "node:crypto" import { constants } from "node:fs" import { lstat, open, readdir, realpath } from "node:fs/promises" @@ -15,7 +15,7 @@ const MaxDiscoveryEntries = 10_000 const MaxHeaderBytes = 64 * 1024 * 1024 const MaxCursorBytes = 1024 * 1024 const MaxDecodedCursorBytes = 16 * 1024 * 1024 -const ProjectionRevision = 5 +const ProjectionRevision = 7 const RecordSchema = Schema.Record(Schema.String, Schema.Unknown) const decodeRecord = Schema.decodeUnknownSync(RecordSchema) const CompactionSchema = Schema.Struct({ @@ -441,7 +441,7 @@ async function validatePendingProjection(handle: Awaited fail("cursor", "Claude checkpoint has no admitted pending conversation record.") const calls = new Map(context.calls) const projected = projectRecord(record, context.order, line.end, sourceObjectId, calls, - cursor.projectionRevision === ProjectionRevision ? ProjectionRevision : 4) + cursor.projectionRevision === 5 ? 5 : cursor.projectionRevision === 6 ? 6 : cursor.projectionRevision === ProjectionRevision ? ProjectionRevision : 4) bindChild(record, calls, delegated, children, cursor.sessionId) if (skip > projected.events.length) fail("cursor", "Claude record checkpoint exceeds its event count.") return @@ -565,7 +565,7 @@ function rootTitle(root: RecordValue): string { } function projectRecord(record: RecordValue, order: number, revision: number, sourceObjectId: string, - calls: Map, projectionRevision: 4 | 5 = ProjectionRevision, includeIdentity = false): { events: Array; partial: boolean } { + calls: Map, projectionRevision: 4 | 5 | 6 | 7 = ProjectionRevision, includeIdentity = false): { events: Array; partial: boolean } { const events: Array = [] let partial = false if (!record.uuid || record.isMeta === true || record.type !== "user" && record.type !== "assistant") return { events, partial } @@ -583,7 +583,7 @@ function projectRecord(record: RecordValue, order: number, revision: number, sou // Source command envelopes need a provider mapping, not fake user prose. if (record.type === "user" && userCommandText(block.text)) { partial = true; continue } update = { sessionUpdate: record.type === "user" ? "user_message_chunk" : "agent_message_chunk", content: { type: "text", text: block.text } } - } else if (projectionRevision === ProjectionRevision && block?.type === "thinking" && record.type === "assistant" && + } else if (projectionRevision >= 5 && block?.type === "thinking" && record.type === "assistant" && typeof block.thinking === "string" && block.thinking.length > 0) { update = { sessionUpdate: "agent_thought_chunk", content: { type: "text", text: block.thinking } } } else if (block?.type === "tool_use" && record.type === "assistant" && typeof block.id === "string" && typeof block.name === "string") { @@ -593,14 +593,15 @@ function projectRecord(record: RecordValue, order: number, revision: number, sou // A copied Skill name needs its original field at the redaction // boundary. An omitted oversized input cannot justify a new copy. const identity = includeIdentity && (block.name !== "Skill" || admittedInput) ? toolIdentity(block) : undefined - update = { sessionUpdate: "tool_call", toolCallId: block.id, title: block.name, status: "pending", kind: toolKind(block.name), + const activity = includeIdentity ? fileActivity(block, admittedInput) : undefined + update = { sessionUpdate: "tool_call", toolCallId: block.id, title: includeIdentity ? toolTitle(block.name) : block.name, status: "pending", kind: toolKind(block.name), ...(admittedInput ? { rawInput: block.input } : {}), - ...(identity ? { _meta: { [ToolIdentityMetaKey]: identity } } : {}) } + ...(identity ? { _meta: { [ToolIdentityMetaKey]: identity, ...(activity === undefined ? {} : { [FileActivityMetaKey]: activity }) } } : {}) } partial = true; fidelity = "partial" } else if (block?.type === "tool_result" && record.type === "user" && typeof block.tool_use_id === "string") { const call = calls.get(block.tool_use_id) if (!call || record.sourceToolAssistantUUID !== undefined && record.sourceToolAssistantUUID !== call.uuid) fail("unsupported", "Claude tool result has no unambiguous call.") - update = { sessionUpdate: "tool_call_update", toolCallId: block.tool_use_id, title: call.name, status: block.is_error === true ? "failed" : "completed", kind: toolKind(call.name), + update = { sessionUpdate: "tool_call_update", toolCallId: block.tool_use_id, title: includeIdentity ? toolTitle(call.name) : call.name, status: block.is_error === true ? "failed" : "completed", kind: toolKind(call.name), ...(isBoundedToolValue(block.content) ? { rawOutput: block.content } : {}) } partial = true; fidelity = "partial" } else if (block?.type === "thinking" && block.thinking === "") { @@ -662,7 +663,7 @@ function decodeCursor(value: string | null): Cursor | undefined { if (Buffer.byteLength(value) > MaxDecodedCursorBytes) throw new Error() const c = Schema.decodeUnknownSync(CursorSchema)(JSON.parse(value)) if (!c.sessionId || !isAbsolute(c.origin) || !Number.isSafeInteger(c.bytes) || c.bytes < 0 || !/^[a-f0-9]{64}$/.test(c.digest)) throw new Error() - if (c.projectionRevision !== undefined && ![2, 3, 4, ProjectionRevision].includes(c.projectionRevision)) throw new Error() + if (c.projectionRevision !== undefined && ![2, 3, 4, 5, 6, ProjectionRevision].includes(c.projectionRevision)) throw new Error() if (c.familyRevision !== undefined && (!Number.isSafeInteger(c.familyRevision) || c.familyRevision < 1)) throw new Error() if (c.familyObservedAt !== undefined && timestamp(c.familyObservedAt) !== c.familyObservedAt) throw new Error() if (c.children) { @@ -822,15 +823,37 @@ async function belongsToProject(archive: Archive, root: RecordValue, signal: Abo const toolKind = (name: string): "read" | "edit" | "execute" | "search" | "other" => name === "Read" ? "read" : ["Write", "Edit", "MultiEdit"].includes(name) ? "edit" : name === "Bash" ? "execute" : ["Grep", "Glob"].includes(name) ? "search" : "other" +const isFileActivity = Schema.is(AcpFileActivity) +function fileActivity(block: RecordValue, admittedInput: boolean): AcpFileActivity | null | undefined { + const operation = toolKind(block.name as string) + if (operation !== "read" && operation !== "edit" && operation !== "search") return undefined + const input = object(block.input) + if (!admittedInput || !input) return null + // Search paths are explicit scopes, never the contents of a pattern or result. + const facts = [{ operation, path: operation === "search" ? input.path : input.file_path }] + return isFileActivity(facts) ? facts : null +} + // Only explicit native tool invocations establish an identity. Reading a Skill // file, mentioning a tool, or retaining an injected command envelope does not. +function toolTitle(name: string): string { + if (name.trim() === "") return "Claude tool" + const bytes = Buffer.from(name) + let end = Math.min(bytes.length, 500) + while (end < bytes.length && (bytes[end]! & 0xc0) === 0x80) end-- + return bytes.subarray(0, end).toString("utf8") +} const isToolIdentity = Schema.is(AcpToolIdentity) function toolIdentity(block: RecordValue): AcpToolIdentity | undefined { if (block.name === "Skill") { const identity = { type: "skill", name: object(block.input)?.skill } return isToolIdentity(identity) ? identity : undefined } - if (typeof block.name !== "string" || !block.name.startsWith("mcp__")) return undefined + if (typeof block.name !== "string") return undefined + if (!block.name.startsWith("mcp__")) { + const identity = { type: "tool", name: block.name } + return isToolIdentity(identity) ? identity : undefined + } const separator = block.name.indexOf("__", 5) if (separator < 0) return undefined const identity = { type: "mcp", server: block.name.slice(5, separator), name: block.name.slice(separator + 2) } @@ -846,7 +869,7 @@ type CaptureStream = { file: string; threadId: string; root: RecordValue; refs: RecordRef[]; nodes: Map; selected: Set latestUsage: Map; children: Child[]; proof: SourceProof; events: number; usage: number; observedAt: string } -const captureProfile = "claude.jsonl.active-path.2" +const captureProfile = "claude.jsonl.active-path.4" const emptyNormalization = (): Normalization => ({ records: new Map(), calls: new Map(), leaf: null, order: 0, continuation: undefined, response: undefined }) const captureFailure = (cause: unknown) => cause instanceof ClaudeArchiveError ? cause : new ClaudeArchiveError({ reason: "io", message: "Could not read the selected Claude source view." }) function captureCheckpoint(value: string | undefined): CaptureCheckpoint | undefined { @@ -1073,8 +1096,8 @@ async function indexCaptureStream(file: string, rootIdentity: RecordValue | unde * publication. Provider branch selection and source integrity stay here. */ export const openClaudeCapture = (archive: Archive, request: SourceOpenRequestV2): Effect.Effect => Effect.tryPromise({ try: async () => { - if (!request.projection.canonicalProfiles?.includes(CanonicalProfileVersion4)) - fail("unsupported", "Upgrade the ATape CLI to retain normalized tool identities with Canonical v4.") + if (!request.projection.canonicalProfiles?.includes(CanonicalProfileVersion5)) + fail("unsupported", "Upgrade the ATape CLI to retain normalized tool identities with Canonical v5.") let readSignal: AbortSignal | undefined const started = performance.now(), check = () => { request.signal.throwIfAborted() @@ -1147,7 +1170,7 @@ export const openClaudeCapture = (archive: Archive, request: SourceOpenRequestV2 const sourceCheckpoint = JSON.stringify({ v: 1, sessionId: origin.sourceId, origin: origin.cwd, rootUuid: origin.originKey, streams: [...proofs.values()] } satisfies CaptureCheckpoint) if (Buffer.byteLength(sourceCheckpoint) > MaxCursorBytes) fail("limit", "Claude source checkpoint exceeds its metadata capacity.") const observedAt = streams.filter(stream => linkedThreads.has(stream.threadId)).map(stream => stream.observedAt).sort().at(-1)! - const header = { profile: captureProfile, canonicalProfileVersion: CanonicalProfileVersion4, origin, sourceCheckpoint, + const header = { profile: captureProfile, canonicalProfileVersion: CanonicalProfileVersion5, origin, sourceCheckpoint, session: { sourceSessionId: origin.sourceId, title: rootTitle(root.root), summary: "Claude Code conversation", insight: "", actor: { name: "User", harness: "Claude Code" }, branch: string(root.root.gitBranch) ?? "", status: "active" as const, captureStatus: "partial" as const, updatedAt: observedAt, reportedEventCount: eventCount }, diff --git a/adapters/claude/src/claudeBackground.test.ts b/adapters/claude/src/claudeBackground.test.ts index 29f6a7e..0ca791c 100644 --- a/adapters/claude/src/claudeBackground.test.ts +++ b/adapters/claude/src/claudeBackground.test.ts @@ -1,4 +1,4 @@ -import { CanonicalProfileVersion4, AdapterCollectionLimits, SourceCaptureHeaderV2, SourceCapturePage, type AdapterOpenContext, type SourceCaptureFrame, type SourceOpenRequestV2 } from "@atape/domain" +import { CanonicalProfileVersion5, AdapterCollectionLimits, SourceCaptureHeaderV2, SourceCapturePage, type AdapterOpenContext, type SourceCaptureFrame, type SourceOpenRequestV2 } from "@atape/domain" import { Effect, Schema } from "effect" import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" @@ -16,7 +16,7 @@ const nativeRoot = await readFile(new URL("root.jsonl", folder), "utf8") const nativeChild = await readFile(new URL("child.jsonl", folder), "utf8") const childId = `claude-agent:${proof.agentId}`, launchId = `${proof.call.receiptUuid}:0` const limits = { rowBytes: 16 * 1024 * 1024, pageBytes: 32 * 1024 * 1024, pageRows: 100, records: 10000, threads: 100, durationMs: 300000 } -const projection = { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: [CanonicalProfileVersion4] } +const projection = { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: [CanonicalProfileVersion5] } const encode = (rows: Row[]) => rows.map(row => JSON.stringify(row) + "\n").join("") const decode = (text: string): Row[] => text.trimEnd().split("\n").map(line => JSON.parse(line)) let directory: string, file: string, childFile: string, context: AdapterOpenContext & { signal: AbortSignal } diff --git a/adapters/claude/src/claudeBackgroundCompatibility.test.ts b/adapters/claude/src/claudeBackgroundCompatibility.test.ts index 3f09733..11c13b1 100644 --- a/adapters/claude/src/claudeBackgroundCompatibility.test.ts +++ b/adapters/claude/src/claudeBackgroundCompatibility.test.ts @@ -1,4 +1,4 @@ -import { CanonicalProfileVersion4, SourceCaptureHeaderV2, SourceCapturePage, type AdapterOpenContext, type SourceCaptureFrame, +import { CanonicalProfileVersion5, SourceCaptureHeaderV2, SourceCapturePage, type AdapterOpenContext, type SourceCaptureFrame, type SourceOpenRequestV2 } from "@atape/domain" import { Schema } from "effect" import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" @@ -13,7 +13,7 @@ const folder = new URL("../fixtures/native-background-child-2.1.263/", import.me const proof = JSON.parse(await readFile(new URL("provenance.json", folder), "utf8")) as Row const root = await readFile(new URL("root.jsonl", folder), "utf8"), child = await readFile(new URL("child.jsonl", folder), "utf8") const limits = { rowBytes: 16 * 1024 * 1024, pageBytes: 32 * 1024 * 1024, pageRows: 100, records: 10000, threads: 100, durationMs: 300000 } -const projection = { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: [CanonicalProfileVersion4] } +const projection = { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: [CanonicalProfileVersion5] } const childId = `claude-agent:${proof.agentId}`, notificationId = `${proof.notification.uuid}:0`, launchId = `${proof.call.receiptUuid}:0` let historical: Awaited> let directory: string, file: string, childFile: string, context: AdapterOpenContext & { signal: AbortSignal } diff --git a/adapters/claude/src/claudeCapture.test.ts b/adapters/claude/src/claudeCapture.test.ts index bfb703f..da99169 100644 --- a/adapters/claude/src/claudeCapture.test.ts +++ b/adapters/claude/src/claudeCapture.test.ts @@ -1,4 +1,4 @@ -import { CanonicalProfileVersion4, ToolIdentityMetaKey, SourceCaptureHeaderV2, SourceCapturePage, SourceCaptureVersion2, AdapterCollectionLimits, +import { CanonicalProfileVersion5, ToolIdentityMetaKey, FileActivityMetaKey, SourceCaptureHeaderV2, SourceCapturePage, SourceCaptureVersion2, AdapterCollectionLimits, type AdapterOpenContext, type SourceOpenRequestV2, type SourceCaptureFrame } from "@atape/domain" import { Effect, Schema } from "effect" import { appendFile, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises" @@ -17,7 +17,7 @@ type Row = Record let directory: string, file: string, context: AdapterOpenContext & { signal: AbortSignal } const sid = "generated-capture-session", encode = (rows: Row[]) => rows.map(row => JSON.stringify(row) + "\n").join("") const limits = { rowBytes: 16 * 1024 * 1024, pageBytes: 32 * 1024 * 1024, pageRows: 100, records: 10000, threads: 100, durationMs: 300000 } -const projection = { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: [CanonicalProfileVersion4] } +const projection = { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: [CanonicalProfileVersion5] } beforeEach(async () => { directory = await mkdtemp(join(tmpdir(), "atape-claude-capture-")); file = join(directory, "session.jsonl") vi.stubEnv("ATAPE_CLAUDE_SESSION_FILE", file) @@ -84,13 +84,13 @@ const nativeFamily = async () => { return { proof, sources, childFile, childId: `claude-agent:${proof.agentId}` } } -it.each([undefined, ["atape.acp-centered.v2"] as const])("requires an explicit Host v4 capability before opening source bytes (%s)", async canonicalProfiles => { +it.each([undefined, ["atape.acp-centered.v2"] as const, ["atape.acp-centered.v4"] as const])("requires an explicit Host v5 capability before opening source bytes (%s)", async canonicalProfiles => { // The source does not exist: capability failure must precede discovery/read. const { canonicalProfiles: _profiles, ...legacyProjection } = projection await expect(capture(request({ projection: { ...legacyProjection, ...(canonicalProfiles ? { canonicalProfiles } : {}) } }))).rejects.toThrow("Upgrade the ATape CLI") }) -it("retains only explicit native MCP and Skill identities without changing source bodies or outcomes", async () => { +it("retains explicit native tool, MCP and Skill identities without changing source bodies or outcomes", async () => { const blocks = [ { type: "tool_use", id: "mcp", name: "mcp__docs__read_page", input: { id: "page" } }, { type: "tool_use", id: "skill", name: "Skill", input: { skill: "plugin:review", args: "private input" } }, @@ -103,7 +103,8 @@ it("retains only explicit native MCP and Skill identities without changing sourc { type: "tool_use", id: "large", name: "Skill", input: { skill: "界".repeat(167) } }, { type: "tool_use", id: "omitted-input", name: "Skill", input: { skill: "private-workflow", args: "x".repeat(65536) } }, { type: "tool_use", id: "boundary", name: "Skill", input: { skill: "界".repeat(166) } }, - { type: "tool_use", id: "malformed-mcp", name: "mcp__docs", input: {} } + { type: "tool_use", id: "malformed-mcp", name: "mcp__docs", input: {} }, + ...["", " Read", "Read ", "Read\u0085", "界".repeat(167)].map((name, i) => ({ type: "tool_use", id: `invalid-${i}`, name, input: {} })) ] const common = { sessionId: sid, cwd: directory, isSidechain: false, timestamp: "2026-10-11T00:00:00Z" } const source = encode([ @@ -116,29 +117,68 @@ it("retains only explicit native MCP and Skill identities without changing sourc ]) await writeFile(file, source) const actual = await capture() - expect(actual.header).toMatchObject({ profile: "claude.jsonl.active-path.2", canonicalProfileVersion: CanonicalProfileVersion4 }) + expect(actual.header).toMatchObject({ profile: "claude.jsonl.active-path.4", canonicalProfileVersion: CanonicalProfileVersion5 }) const calls = actual.events.filter(event => event.update.sessionUpdate === "tool_call") expect(calls.filter(event => "_meta" in event.update).map(event => event.update)).toMatchObject([ { toolCallId: "mcp", status: "pending", _meta: { [ToolIdentityMetaKey]: { type: "mcp", server: "docs", name: "read_page" } } }, { toolCallId: "skill", status: "pending", _meta: { [ToolIdentityMetaKey]: { type: "skill", name: "plugin:review" } } }, + { toolCallId: "file", title: "Read", status: "pending", _meta: { [ToolIdentityMetaKey]: { type: "tool", name: "Read" } } }, + { toolCallId: "mention", title: "Run Skill workflow", status: "pending", _meta: { [ToolIdentityMetaKey]: { type: "tool", name: "Run Skill workflow" } } }, { toolCallId: "boundary", status: "pending", _meta: { [ToolIdentityMetaKey]: { type: "skill", name: "界".repeat(166) } } } ]) expect(calls.find(event => "toolCallId" in event.update && event.update.toolCallId === "skill")?.update) .toMatchObject({ rawInput: { skill: "plugin:review", args: "private input" } }) - expect(calls.find(event => "toolCallId" in event.update && event.update.toolCallId === "file")?.update) - .toMatchObject({ title: "Read", rawInput: { file_path: "/tmp/SKILL.md", skill: "not-an-invocation" } }) - expect(calls.find(event => "toolCallId" in event.update && event.update.toolCallId === "file")?.update).not.toHaveProperty("_meta") - expect(actual.events.every(event => !JSON.stringify(event.update).includes('"atape.dev/file-activity"'))).toBe(true) const omitted = calls.find(event => "toolCallId" in event.update && event.update.toolCallId === "omitted-input")!.update expect(omitted).not.toHaveProperty("rawInput"); expect(omitted).not.toHaveProperty("_meta") expect(actual.events.filter(event => event.update.sessionUpdate === "tool_call_update").map(event => event.update)).toMatchObject([ { toolCallId: "mcp", status: "completed" }, { toolCallId: "skill", status: "failed" } ]) expect(actual.events.filter(event => event.update.sessionUpdate === "tool_call_update").every(event => !("_meta" in event.update))).toBe(true) + expect(calls.filter(event => "toolCallId" in event.update && event.update.toolCallId.startsWith("invalid-")).every(event => !("_meta" in event.update))).toBe(true) expect(rawText(actual.frames)).toBe(source) + expect(await capture(request({ priorCheckpoint: actual.header.sourceCheckpoint, priorThreads: actual.header.threads }))).toEqual(actual) expect(await capture(request({ rawEnabled: false }))).toMatchObject({ header: actual.header, events: actual.events }) }) +it("records only admitted native file paths and explicit search scopes", async () => { + const blocks = [ + { type: "tool_use", id: "read", name: "Read", input: { file_path: "./src/main.ts" } }, + { type: "tool_use", id: "write", name: "Write", input: { file_path: "/project/new.ts", content: "new body" } }, + { type: "tool_use", id: "edit", name: "Edit", input: { file_path: "src/main.ts", old_string: "a", new_string: "b" } }, + { type: "tool_use", id: "multi", name: "MultiEdit", input: { file_path: "src/main.ts", edits: [] } }, + { type: "tool_use", id: "grep", name: "Grep", input: { path: "src", pattern: "looks/like/a/path.ts" } }, + { type: "tool_use", id: "glob", name: "Glob", input: { path: "./", pattern: "**/*.ts" } }, + { type: "tool_use", id: "unknown-scope", name: "Glob", input: { pattern: "src/**/*.ts" } }, + { type: "tool_use", id: "shell", name: "Bash", input: { command: "cat src/main.ts; rg TODO src", file_path: "invented.ts" } }, + { type: "tool_use", id: "mcp", name: "mcp__fs__read", input: { file_path: "invented.ts" } }, + { type: "tool_use", id: "extension", name: "ReadCustom", input: { file_path: "invented.ts" } }, + { type: "tool_use", id: "oversized", name: "Write", input: { file_path: "omitted.ts", content: "x".repeat(65536) } }, + ...["", "bad\npath", "bad\0path", "\ud800", "界".repeat(683)].map((file_path, index) => + ({ type: "tool_use", id: `invalid-${index}`, name: "Read", input: { file_path } })), + { type: "tool_use", id: "spaces", name: "Read", input: { file_path: " keep spaces " } } + ] + const sourceRows = rows().slice(0, 2) + sourceRows[1]!.message.content = blocks + const source = encode(sourceRows); await writeFile(file, source) + const actual = await capture() + expect(actual.header.profile).toBe("claude.jsonl.active-path.4") + const facts = (id: string) => { + const update = actual.events.find(event => "toolCallId" in event.update && event.update.toolCallId === id)!.update + return "_meta" in update ? update._meta?.[FileActivityMetaKey] : undefined + } + expect(facts("read")).toEqual([{ operation: "read", path: "./src/main.ts" }]) + for (const [id, path] of [["write", "/project/new.ts"], ["edit", "src/main.ts"], ["multi", "src/main.ts"]]) + expect(facts(id!)).toEqual([{ operation: "edit", path }]) + expect(facts("grep")).toEqual([{ operation: "search", path: "src" }]) + expect(facts("glob")).toEqual([{ operation: "search", path: "./" }]) + expect(facts("spaces")).toEqual([{ operation: "read", path: " keep spaces " }]) + for (const id of ["unknown-scope", "oversized", ...blocks.filter(block => block.id.startsWith("invalid-")).map(block => block.id)]) + expect(facts(id)).toBeNull() + for (const id of ["shell", "mcp", "extension"]) expect(facts(id)).toBeUndefined() + expect(rawText(actual.frames)).toBe(source) + expect(await capture(request({ priorCheckpoint: actual.header.sourceCheckpoint, priorThreads: actual.header.threads }))).toEqual(actual) +}) + it("selects a generated truncating-resume branch and retains all physical Raw", async () => { const source = encode(rows()); await writeFile(file, source) const actual = await capture() @@ -570,6 +610,22 @@ const legacyFirstPage = async (values: Row[], target = file) => { // separate from the unchanged genuine previous-factory cursor acceptance cases. const legacyState = (cursor: string): Row => JSON.parse(cursor.startsWith("z3:") ? inflateRawSync(Buffer.from(cursor.slice(3), "base64url")).toString("utf8") : cursor) + +it.each([5, 6])("validates generated retained projection %s pending slots before the new file projection", async projectionRevision => { + const values = rows().slice(0, 2) + values[0]!.message.content = [{ type: "text", text: "first root part" }, { type: "text", text: "second root part" }] + values[1]!.message.content = [{ type: "tool_use", id: "read", name: "Read", input: { file_path: "retained.ts" } }] + const previous = await legacyFirstPage(values), state = legacyState(previous.nextCursor) + state.sessions[0].checkpoint.projectionRevision = projectionRevision + const checkpoint = JSON.stringify(state) + const current = await capture(request({ legacyCheckpoint: checkpoint })) + expect(current.events.map(event => event.sourceEventId)).toEqual(["root:0", "root:1", "first:0"]) + expect(current.events[2]!.update).toMatchObject({ _meta: { [FileActivityMetaKey]: [{ operation: "read", path: "retained.ts" }] } }) + expect(rawText(current.frames)).toBe(encode(values)) + state.sessions[0].checkpoint.stream.eventSkip = 3 + await expect(capture(request({ legacyCheckpoint: JSON.stringify(state) }))).rejects.toThrow("event count") +}) + it.each([false, true])("resolves a genuine first-root partial page with no acknowledged UUID (bookkeeping %s)", async bookkeeping => { const values = rows().slice(0, 2) values[0]!.message.content = [{ type: "text", text: "first root part" }, { type: "text", text: "second root part" }] diff --git a/adapters/claude/src/claudeGenericCompaction.test.ts b/adapters/claude/src/claudeGenericCompaction.test.ts index 6991a2a..6d3a460 100644 --- a/adapters/claude/src/claudeGenericCompaction.test.ts +++ b/adapters/claude/src/claudeGenericCompaction.test.ts @@ -167,7 +167,7 @@ const capture = async (priorCheckpoint?: string, rawEnabled = true, priorThreads try { const root = (await readFile(file, "utf8")).split("\n").slice(0, -1).filter(Boolean).map(line => JSON.parse(line) as Row).find(row => typeof row.uuid === "string")! const view = await runtime.sourceCapture.open({ sourceId: root.sessionId, rawEnabled, priorThreads, ...(priorCheckpoint === undefined ? {} : { priorCheckpoint }), limits, - projection: { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: ["atape.acp-centered.v4"] }, signal: context.signal }) + projection: { events: 100000, usage: 100000, pageItems: 1, pageBytes: 32 * 1024 * 1024, canonicalProfiles: ["atape.acp-centered.v5"] }, signal: context.signal }) try { const header = Schema.decodeUnknownSync(SourceCaptureHeaderV2)(view), frames: SourceCaptureFrame[] = [] for (;;) { const page = Schema.decodeUnknownSync(SourceCapturePage)(await view.read(context.signal)); frames.push(...page.frames); if (page.done) break } diff --git a/adapters/codebuddy/package.json b/adapters/codebuddy/package.json index 5d58db4..3849fbb 100644 --- a/adapters/codebuddy/package.json +++ b/adapters/codebuddy/package.json @@ -1,6 +1,6 @@ { "name": "@atape/adapter-codebuddy", - "version": "0.5.11", + "version": "0.5.12", "type": "module", "exports": { ".": "./dist/index.js" diff --git a/adapters/codex/README.md b/adapters/codex/README.md index 60941d2..e452978 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -1,11 +1,14 @@ # ATape Codex Adapter - -The 0.5.11 `legacy-bridge` release retains the published 0.5.10 capture -Implementation while the CLI gains target-owned automatic updates. -The Codex Adapter keeps Canonical v3 and projection/cursor v5. Ordinary native-tool identity and file-activity capture are -planned for the subsequent `target-owned` release. See +The 0.5.12 `target-owned` Adapter restores native tool identity and recorded +file activity through Canonical v5 and projection/cursor v7. The 0.5.11 +`legacy-bridge` retains its genuine Canonical v3/projection v5 capture for older +clients. The verified target CLI owns preparation of this v5 Adapter; see [ADR-0126](../../docs/architecture/adr/0126-target-owned-managed-updates.md). +See the [0.5.12 release notes](../../docs/releases/v0.5.12.md) for publication, +deployment and acceptance evidence. + +This package lets the ATape Collector read Codex rollout archives that belong to a configured local Project. It is installed independently from the base CLI and loaded only while an enabled Project is collected. ```sh atape @@ -17,10 +20,19 @@ the affected Projects. Installing the package alone does not authorize capture. The Adapter reads from `ATAPE_CODEX_HOME`, then `CODEX_HOME`, and otherwise `~/.codex`. It only reports sessions whose recorded working directory or Git repository matches the configured Project. Provider files remain untouched when ATape captures or removes local Project configuration. -This Adapter requires an updated ATape CLI and a Server supporting explicit unknown source clocks. Missing or invalid conversation timestamps remain unknown. Filesystem modification times drive collection progress and provide a stable legacy Raw capture basis for retries; they do not become conversation timestamps. Upgrading replays retained Canonical source history once while keeping acknowledged Raw byte offsets. +This Adapter requires an updated ATape CLI and a Server supporting Canonical v5 with explicit unknown source clocks and stable tool identities and typed file activity. Missing or invalid conversation timestamps remain unknown. Filesystem modification times drive collection progress and provide a stable legacy Raw capture basis for retries; they do not become conversation timestamps. Upgrading from projection 5 or 6 to 7 replays retained Canonical source history once while keeping acknowledged Raw byte offsets and assigned source ordering. New cursors require the matching newer Adapter; recover forward without deleting state. + +Native command, file-change and image-view items have stable tool identities independent of their display titles. Function and custom calls use their explicit native name; MCP items preserve their explicit server/tool pair and original input fields for upload redaction. Missing or invalid names and unknown Extension items stay unidentified, and absent outcomes remain unknown. Function-call kind remains the existing display classification; it does not establish identity. Recorded path facts do not establish successful file changes. Installation does not run npm lifecycle scripts. The release tarball therefore contains a ready-to-run `dist/index.js` bundle and requires Node.js 24 or newer. This package is available under the MIT License. Choose Codex in Tools and updates → Choose tools to sync to install and enable it. + +The file activity projection uses native +`CommandExecution.parsed_cmd` read/search paths, typed `FileChange.changes` keys +and move destinations, and `ImageView.path`. Search paths are scopes rather than +matching files. Shell commands, old patch envelopes and MCP inputs are not +classified. Missing, malformed, oversized or redacted paths remain unknown. +See the [Adapter guide](../../docs/adapters/codex.md#explicit-file-activity). diff --git a/adapters/codex/package.json b/adapters/codex/package.json index a3157cf..984c139 100644 --- a/adapters/codex/package.json +++ b/adapters/codex/package.json @@ -1,6 +1,6 @@ { "name": "@atape/adapter-codex", - "version": "0.5.11", + "version": "0.5.12", "description": "ATape Adapter for local Codex rollout archives", "license": "MIT", "type": "module", @@ -56,7 +56,7 @@ "protocolVersion": "atape.adapter.v1alpha1", "gitAttribution": "atape.git-attribution.v1", "rawCapturePolicy": "atape.raw-capture.v1", - "legacyCanonicalProfile": "atape.acp-centered.v3", + "legacyCanonicalProfile": "atape.acp-centered.v5", "adapterId": "codex", "displayName": "Codex", "entry": "./dist/index.js", diff --git a/adapters/codex/src/codexArchive.test.ts b/adapters/codex/src/codexArchive.test.ts index afca4d4..8421639 100644 --- a/adapters/codex/src/codexArchive.test.ts +++ b/adapters/codex/src/codexArchive.test.ts @@ -2,7 +2,9 @@ import { AdapterCollectionLimits, type AdapterCollectionLimitValues, AdapterProtocolVersion, - CanonicalProfileVersion3, + CanonicalProfileVersion5, + ToolIdentityMetaKey, + FileActivityMetaKey, type AdapterCollectionPage, type AdapterSourceProgress, type LegacyAdapterRuntime @@ -25,28 +27,9 @@ afterEach(async () => { }) describe("Codex Adapter", () => { - it("keeps the bridge's genuine v3 projection without ordinary identities or file facts", async () => { - const root = await makeEmptyFixture() - await writeJsonl(join(root.sessionsDirectory, "bridge.jsonl"), [sessionMeta({ id: "bridge", cwd: root.project }), - itemCompleted("2026-09-05T00:01:00Z", "bridge", { type: "CommandExecution", id: "command", command: "cat src/main.ts", - exit_code: 0, parsed_cmd: [{ type: "read", path: "src/main.ts" }] }), - itemCompleted("2026-09-05T00:02:00Z", "bridge", { type: "ImageView", id: "image", path: "assets/example.png" })]) - const runtime = await openAdapter(root.project, "directory") - try { - const page = await collect(runtime), observation = requiredObservation(page) - expect(runtime.canonicalProfileVersion).toBe(CanonicalProfileVersion3) - expect(observation.events.map(event => event.update)).toMatchObject([ - { toolCallId: "command", title: "cat src/main.ts", status: "completed" }, - { toolCallId: "image", title: "View image example.png" } - ]) - expect(observation.events.every(event => event.projectionRevision === 5 && !("_meta" in event.update) && !("rawInput" in event.update))).toBe(true) - expect(JSON.parse(Buffer.from(page.nextCursor!, "base64url").toString("utf8"))).toMatchObject({ v: 5, eventProjectionVersion: 5 }) - } finally { await runtime.close?.() } - }) - - it("refuses a Host without explicit nullable-clock support before opening sources", async () => { + it.each([undefined, ["atape.acp-centered.v3"]] as const)("refuses a Host without explicit stable identity support before opening sources (%s)", async legacyCanonicalProfiles => { await expect(createAtapeAdapter({ protocolVersion: AdapterProtocolVersion, - adapter: { id: "codex", version: "0.1.0" }, project: { id: "project", type: "directory", path: "/missing-atape-clock-test" }, + adapter: { id: "codex", version: "0.1.0" }, project: { id: "project", type: "directory", path: "/missing-atape-clock-test" }, ...(legacyCanonicalProfiles ? { legacyCanonicalProfiles } : {}), signal: AbortSignal.timeout(5_000) })).rejects.toThrow("explicit source clock quality") }) @@ -56,7 +39,7 @@ describe("Codex Adapter", () => { await writeJsonl(join(root.sessionsDirectory, "lazy.jsonl"), [sessionMeta({ id: "lazy", cwd: laterProject }), itemCompleted("2026-09-05T00:01:00Z", "lazy", { type: "AgentMessage", id: "answer", content: [{ type: "output_text", text: "ready" }] })]) const runtime = await openAdapter(laterProject, "directory") - expect(runtime.canonicalProfileVersion).toBe(CanonicalProfileVersion3) + expect(runtime.canonicalProfileVersion).toBe(CanonicalProfileVersion5) await expect(collect(runtime)).rejects.toThrow("Could not open the Codex archive") await mkdir(laterProject) const cancellation = new AbortController() @@ -331,7 +314,9 @@ describe("Codex Adapter", () => { expect(rejected.sourceFailures).toContainEqual(expect.objectContaining({ reason: "limit" })) }) - it.each(["canonical", "raw"] as const)("upgrades an active projection 4 %s cursor without losing its Raw receipt", async phase => { + it.each([ + ["canonical", 4], ["raw", 4], ["canonical", 5], ["raw", 5] + ] as const)("upgrades an active %s projection %s cursor without losing its Raw receipt", async (phase, projectionVersion) => { const root = await makeEmptyFixture() const path = join(root.sessionsDirectory, "upgrade.jsonl") await writeJsonl(path, [sessionMeta({ id: "upgrade", cwd: root.project }), @@ -344,7 +329,8 @@ describe("Codex Adapter", () => { const page = phase === "canonical" ? first : await collect(runtime, first.nextCursor, [], limits) const state = JSON.parse(Buffer.from(page.nextCursor!, "base64url").toString("utf8")) expect(state.active.phase).toBe(phase) - state.eventProjectionVersion = 4 + state.eventProjectionVersion = projectionVersion + if (projectionVersion === 5) state.v = 5 state.active.revision += 100 const previousRevision = state.active.revision delete state.active.sourceUpdatedAt @@ -357,7 +343,7 @@ describe("Codex Adapter", () => { expect(upgraded.session.updatedAt).toBe("2026-09-05T00:03:00Z") if (phase === "canonical") { expect(upgraded.events.map(event => event.sourceEventId)).toEqual(["event-1"]) - expect(upgraded.events[0]?.projectionRevision).toBe(5) + expect(upgraded.events[0]?.projectionRevision).toBe(7) } else { expect(upgraded.events).toEqual([]) expect(upgraded.rawSegments[0]?.sourceOffset).toBe(progress[0]?.sourceOffset) @@ -757,6 +743,181 @@ describe("Codex Adapter", () => { .toContain("provider-only-tool-output") }) + it("uses explicit native tool identity independently of display titles and rejects malformed names", async () => { + const root = await makeEmptyFixture() + const native = [ + { type: "CommandExecution", id: "shell-one", command: ["echo", "one"] }, + { type: "CommandExecution", id: "shell-two", command: ["echo", "two"], exit_code: 1 }, + { type: "McpToolCall", id: "mcp", server: "docs", tool: "inspect", status: "completed" }, + { type: "McpToolCall", id: "bad-mcp", server: " docs", tool: "inspect" }, + { type: "FileChange", id: "file" }, { type: "ImageView", id: "image", path: "/tmp/fixture.png" }, + { type: "Extension", id: "extension", kind: "Display only" }, + { type: "Extension", id: "named-extension", kind: "Displayed extension", name: "native_extension" } + ] + const calls = [ + { type: "function_call", id: "function", name: "exec_command" }, + { type: "custom_tool_call", id: "custom", name: "apply_patch" }, + { type: "function_call", id: "missing" }, + ...["", " exec", "exec ", "exec\u0085", "界".repeat(167)].map((name, i) => ({ type: "function_call", id: `invalid-${i}`, name })) + ] + await writeJsonl(join(root.sessionsDirectory, "identities.jsonl"), [sessionMeta({ id: "identities", cwd: root.project }), + ...native.map(item => itemCompleted("2026-09-05T00:01:00Z", "identities", item))]) + await writeJsonl(join(root.sessionsDirectory, "response-identities.jsonl"), [sessionMeta({ id: "identities", cwd: root.project }), + ...calls.map(item => responseItem("2026-09-05T00:01:00Z", item))]) + const observation = requiredObservation(await collect(await openAdapter(root.project, "directory"))) + const update = (id: string) => observation.events.find(event => event.sourceEventId === id)!.update + expect(update("shell-one")).toMatchObject({ title: "echo\none", _meta: { [ToolIdentityMetaKey]: { type: "tool", name: "CommandExecution" } } }) + expect(update("shell-two")).toMatchObject({ title: "echo\ntwo", status: "failed", _meta: { [ToolIdentityMetaKey]: { type: "tool", name: "CommandExecution" } } }) + expect(update("shell-one")).not.toHaveProperty("status") + expect(update("mcp")).toMatchObject({ title: "docs/inspect", rawInput: { server: "docs", tool: "inspect" }, + _meta: { [ToolIdentityMetaKey]: { type: "mcp", server: "docs", name: "inspect" } } }) + expect(update("bad-mcp")).not.toHaveProperty("_meta") + expect(update("bad-mcp")).not.toHaveProperty("rawInput") + expect(update("named-extension")).toMatchObject({ title: "Displayed extension" }) + expect(update("named-extension")).not.toHaveProperty("rawInput") + for (const [id, name] of [["file", "FileChange"], ["image", "ImageView"], ["function", "exec_command"], ["custom", "apply_patch"]]) + expect(update(id!)).toMatchObject({ _meta: { [ToolIdentityMetaKey]: { type: "tool", name } } }) + for (const id of ["extension", "named-extension", "missing", ...calls.filter(call => call.id.startsWith("invalid-")).map(call => call.id)]) + expect(update(id)).not.toHaveProperty("_meta") + }) + + it("records typed file changes, image paths and native parsed command scopes without guessing shell text", async () => { + const root = await makeEmptyFixture(), sessionId = "file-activity" + // Controlled equivalents of Confab's modern native item test cases. The + // command classification and per-path changes are recorded source facts. + const items = [ + { type: "CommandExecution", id: "command", command: ["sh", "-c", "cat guessed.ts; rg TODO guessed"], status: "completed", parsed_cmd: [ + { type: "read", path: "src/app.ts", cmd: "sed -n 1,20p src/app.ts" }, + { type: "read", path: "src/app.ts", cmd: "cat src/app.ts" }, + { type: "search", path: "src", query: "TODO" }, + { type: "unknown", path: "invented.ts" }, { type: "list_files", path: "also-not-a-read" } + ] }, + { type: "FileChange", id: "changes", status: "failed", changes: { + "src/app.ts": { type: "update", unified_diff: "-a\n+b", move_path: "src/renamed.ts" }, + "README.md": { type: "add", content: "# README" }, + "removed.ts": { type: "delete" } + } }, + { type: "ImageView", id: "image", path: "/project/screenshot.png" }, + { type: "McpToolCall", id: "mcp", server: "fs", tool: "read", arguments: { path: "invented.ts" } }, + { type: "FileChange", id: "empty", changes: {} }, + { type: "CommandExecution", id: "shell-only", command: ["cat", "invented.ts"] }, + { type: "FileChange", id: "oversized", changes: { "omitted.ts": { type: "add", content: "x".repeat(65536) } } }, + { type: "CommandExecution", id: "too-many", parsed_cmd: Array.from({ length: 65 }, (_, n) => ({ type: "read", path: `file-${n}.ts` })) }, + { type: "CommandExecution", id: "too-wide", parsed_cmd: [0, 1, 2].map(n => ({ type: "read", path: `${n}${"x".repeat(1500)}` })) }, + { type: "CommandExecution", id: "partial-path", parsed_cmd: [{ type: "read", path: "known.ts" }, { type: "search" }] }, + { type: "FileChange", id: "unknown-change", changes: { "invented.ts": { type: "new-format" } } }, + ...["", "bad\npath", "bad\0path", "\ud800", "界".repeat(683)].map((path, n) => ({ type: "ImageView", id: `invalid-${n}`, path })) + ] + const responses = [ + { type: "function_call", id: "old-shell", name: "exec_command", arguments: JSON.stringify({ cmd: "cat invented.ts" }) }, + { type: "custom_tool_call", id: "old-patch", name: "apply_patch", input: "*** Begin Patch\n*** Add File: invented.ts\n+x\n*** End Patch" } + ] + await writeJsonl(join(root.sessionsDirectory, "files.jsonl"), [sessionMeta({ id: sessionId, cwd: root.project }), + ...items.map(item => itemCompleted("2026-09-05T00:01:00Z", sessionId, item))]) + await writeJsonl(join(root.sessionsDirectory, "response-files.jsonl"), [sessionMeta({ id: sessionId, cwd: root.project }), + ...responses.map(payload => responseItem("2026-09-05T00:02:00Z", payload))]) + const page = await collect(await openAdapter(root.project, "directory")) + const events = requiredObservation(page).events + const update = (id: string) => events.find(event => event.sourceEventId === id)!.update + const facts = (id: string) => { const value = update(id); return "_meta" in value ? value._meta?.[FileActivityMetaKey] : undefined } + expect(facts("command")).toEqual([{ operation: "read", path: "src/app.ts" }, { operation: "search", path: "src" }]) + expect(update("command")).toMatchObject({ kind: "execute", rawInput: { parsed_cmd: items[0]!.parsed_cmd } }) + expect(facts("changes")).toEqual([ + { operation: "edit", path: "README.md" }, { operation: "edit", path: "removed.ts" }, + { operation: "edit", path: "src/app.ts" }, { operation: "edit", path: "src/renamed.ts" } + ]) + expect(update("changes")).toMatchObject({ status: "failed", rawInput: { changes: items[1]!.changes } }) + expect(facts("image")).toEqual([{ operation: "read", path: "/project/screenshot.png" }]) + expect(update("image")).toMatchObject({ title: "View image", rawInput: { path: "/project/screenshot.png" } }) + expect(facts("empty")).toEqual([]) + for (const id of ["shell-only", "oversized", "too-many", "too-wide", "partial-path", "unknown-change", ...items.filter(item => item.id.startsWith("invalid-")).map(item => item.id)]) + expect(facts(id)).toBeNull() + for (const id of ["old-shell", "old-patch", "mcp"]) expect(facts(id)).toBeUndefined() + expect(update("oversized")).not.toHaveProperty("rawInput") + }) + + it.each([5, 6])("replays a retained projection %s checkpoint once with stable IDs, source ranks and acknowledged Raw offsets", async previousProjection => { + const root = await makeEmptyFixture(), path = join(root.sessionsDirectory, "tool-replay.jsonl") + const source = [sessionMeta({ id: "tool-replay", cwd: root.project }), + itemCompleted("2026-09-05T00:01:00Z", "tool-replay", { type: "CommandExecution", id: "command", command: ["echo", "before"], parsed_cmd: [{ type: "read", path: "src/before.ts" }] })] + await writeJsonl(path, source) + const originalRuntime = await openAdapter(root.project, "directory") + let cursor: string | null = null, progress: ReadonlyArray = [] + const originalEvents: AdapterCollectionPage["observations"][number]["events"][number][] = [] + for (let i = 0; i < 20; i++) { + const page = await collect(originalRuntime, cursor, progress) + for (const observation of page.observations) { + originalEvents.push(...observation.events) + progress = mergeProgress(progress, observation.session.sourceSessionId, observation.rawSegments) + } + cursor = page.nextCursor + if (!page.hasMore) break + } + expect(progress[0]?.sourceOffset).toBeGreaterThan(0) + const previous = JSON.parse(Buffer.from(cursor!, "base64url").toString("utf8")) + previous.v = previousProjection; previous.eventProjectionVersion = previousProjection + const ranks = previous.sourceRanks, priorRevision = previous.canonicalProgress[0].revision + cursor = Buffer.from(JSON.stringify(previous)).toString("base64url") + const runtime = await openAdapter(root.project, "directory"), replayed = [] + for (let i = 0; i < 20; i++) { + const page = await collect(runtime, cursor, progress) + replayed.push(...page.observations.flatMap(observation => observation.events)) + expect(page.observations.flatMap(observation => observation.rawSegments)).toEqual([]) + cursor = page.nextCursor + if (!page.hasMore) break + } + expect(replayed.map(event => [event.sourceEventId, event.sourceOrder, event.eventIndex, event.rawRef])) + .toEqual(originalEvents.map(event => [event.sourceEventId, event.sourceOrder, event.eventIndex, event.rawRef])) + expect(replayed[0]).toMatchObject({ projectionRevision: 7, update: { _meta: { [ToolIdentityMetaKey]: { type: "tool", name: "CommandExecution" } } } }) + expect(replayed[0]).toMatchObject({ update: { _meta: { [FileActivityMetaKey]: [{ operation: "read", path: "src/before.ts" }] } } }) + expect(replayed[0]!.revision).toBeGreaterThan(priorRevision) + expect(JSON.parse(Buffer.from(cursor!, "base64url").toString("utf8"))).toMatchObject({ v: 7, sourceRanks: ranks }) + const noop = await collect(await openAdapter(root.project, "directory"), cursor, progress) + expect(noop.observations).toEqual([]) + await appendFile(path, JSON.stringify(itemCompleted("2026-09-05T00:02:00Z", "tool-replay", { + type: "CommandExecution", id: "appended", command: ["echo", "after"], status: "completed" })) + "\n") + const changedTime = new Date(Date.now() + 1_000); await utimes(path, changedTime, changedTime) + const nextRuntime = await openAdapter(root.project, "directory"), appended = [], raw = [] + cursor = noop.nextCursor + for (let i = 0; i < 20; i++) { + const page = await collect(nextRuntime, cursor, progress) + appended.push(...page.observations.flatMap(observation => observation.events)) + for (const observation of page.observations) { + raw.push(...observation.rawSegments) + progress = mergeProgress(progress, observation.session.sourceSessionId, observation.rawSegments) + } + cursor = page.nextCursor + if (!page.hasMore) break + } + expect(appended.map(event => event.sourceEventId)).toEqual(["appended"]) + expect(appended[0]?.update).toMatchObject({ _meta: { [ToolIdentityMetaKey]: { type: "tool", name: "CommandExecution" } } }) + expect(raw[0]?.sourceOffset).toBe(previous.canonicalProgress[0].files[0].size) + expect(raw.map(segment => segment.content).join("")).toContain('"id":"appended"') + }) + + it("clears earlier path facts when a complete update for the same native call has an unknown path", async () => { + const root = await makeEmptyFixture(), path = join(root.sessionsDirectory, "path-update.jsonl") + const firstItem = { type: "ImageView", id: "image", path: "private-original-image.png", status: "running" } + await writeJsonl(path, [sessionMeta({ id: "path-update", cwd: root.project }), itemCompleted("2026-09-05T00:01:00Z", "path-update", firstItem)]) + const first = await collect(await openAdapter(root.project, "directory")) + const original = requiredObservation(first).events[0]! + expect(original.update).toMatchObject({ title: "View image", _meta: { [FileActivityMetaKey]: [{ operation: "read", path: "private-original-image.png" }] } }) + let cursor = first.nextCursor, progress: ReadonlyArray = [] + for (let i = 0; i < 20; i++) { + const page = await collect(await openAdapter(root.project, "directory"), cursor, progress) + for (const observation of page.observations) progress = mergeProgress(progress, observation.session.sourceSessionId, observation.rawSegments) + cursor = page.nextCursor + if (!page.hasMore) break + } + await appendFile(path, JSON.stringify(itemCompleted("2026-09-05T00:02:00Z", "path-update", { type: "ImageView", id: "image", status: "completed" })) + "\n") + const modified = new Date(Date.now() + 1000); await utimes(path, modified, modified) + const changed = requiredObservation(await collect(await openAdapter(root.project, "directory"), cursor, progress)).events[0]! + expect(changed.sourceEventId).toBe(original.sourceEventId) + expect(changed.revision).toBeGreaterThan(original.revision) + expect(changed.update).toMatchObject({ title: "View image", status: "completed", _meta: { [FileActivityMetaKey]: null } }) + expect(JSON.stringify(changed.update)).not.toContain("private-original-image.png") + }) + it("preserves unknown tool outcomes instead of inferring success or failure", async () => { const root = await makeEmptyFixture() await writeJsonl(join(root.sessionsDirectory, "tool-status.jsonl"), [ @@ -989,7 +1150,7 @@ describe("Codex Adapter", () => { const firstObservation = requiredObservation(first) expect(firstObservation.session.sourceSessionId).toBe("first-session") expect(firstObservation.rawSegments).toEqual([]) - expect(firstObservation.session.revision).toBe(firstModified.getTime() * 1_000 * 2 + 12) + expect(firstObservation.session.revision).toBe(firstModified.getTime() * 1_000 * 2 + 14) expect(first.hasMore).toBe(true) const second = await collect(runtime, first.nextCursor) const secondObservation = requiredObservation(second) @@ -1012,7 +1173,7 @@ describe("Codex Adapter", () => { expect(requiredObservation(replay).session.sourceSessionId).toBe("first-session") expect(replay.hasMore).toBe(true) if (replay.nextCursor === null) throw new Error("Expected migrated Cursor") - expect(JSON.parse(Buffer.from(replay.nextCursor, "base64url").toString("utf8"))).toMatchObject({ v: 5 }) + expect(JSON.parse(Buffer.from(replay.nextCursor, "base64url").toString("utf8"))).toMatchObject({ v: 7 }) } const stalled = await collect(runtime, null, [], { @@ -1031,7 +1192,7 @@ describe("Codex Adapter", () => { })).toString("base64url")) expect(requiredObservation(migrated).events).not.toEqual([]) if (migrated.nextCursor === null) throw new Error("Expected migrated Cursor") - expect(JSON.parse(Buffer.from(migrated.nextCursor, "base64url").toString("utf8"))).toMatchObject({ v: 5 }) + expect(JSON.parse(Buffer.from(migrated.nextCursor, "base64url").toString("utf8"))).toMatchObject({ v: 7 }) }) it("prefers the latest valid Codex title index record", async () => { @@ -1059,7 +1220,7 @@ describe("Codex Adapter", () => { expect(observation.session.title).toBe("Checkout accessibility review") expect(observation.session.updatedAt).toBe("2026-09-05T00:02:00.000Z") - expect(observation.session.revision).toBe(rolloutModified.getTime() * 1_000 * 2 + 12) + expect(observation.session.revision).toBe(rolloutModified.getTime() * 1_000 * 2 + 14) }) it("collects a title-only rename without new rollout or Raw bytes", async () => { @@ -1426,7 +1587,7 @@ describe("Codex Adapter", () => { state.canonicalProgress[0].revision = priorRevision let page = await collect(await openAdapter(fixture.project, "directory"), Buffer.from(JSON.stringify(state)).toString("base64url"), progress) expect(requiredObservation(page).events.map(e => e.sourceEventId)).toEqual(requiredObservation(first).events.map(e => e.sourceEventId)) - expect(requiredObservation(page).events.every(event => event.projectionRevision === 5)).toBe(true) + expect(requiredObservation(page).events.every(event => event.projectionRevision === 7)).toBe(true) expect(requiredObservation(page).session.revision).toBeGreaterThan(priorRevision) for (let i = 0; i < 10; i++) { expect(page.observations.flatMap(o => o.rawSegments)).toEqual([]) @@ -1732,7 +1893,7 @@ const makeEmptyFixture = async () => { } const openAdapter = (project: string, type: "git" | "directory", gitAttribution?: import("@atape/domain").AdapterOpenContext["gitAttribution"]) => createAtapeAdapter({ - legacyCanonicalProfiles: [CanonicalProfileVersion3], + legacyCanonicalProfiles: [CanonicalProfileVersion5], ...(gitAttribution === undefined ? {} : { gitAttribution }), protocolVersion: AdapterProtocolVersion, adapter: { id: "codex", version: "0.1.0" }, diff --git a/adapters/codex/src/codexArchive.ts b/adapters/codex/src/codexArchive.ts index d9e8548..f76bc4b 100644 --- a/adapters/codex/src/codexArchive.ts +++ b/adapters/codex/src/codexArchive.ts @@ -11,7 +11,7 @@ import type { AcpSessionUpdate } from "@atape/domain" import { createHash } from "node:crypto" -import { AdapterThread as AdapterThreadSchema, GitAttributionVersion, MaxSourceFailures, type AdapterSourceFailure } from "@atape/domain" +import { AcpFileActivity, AcpToolIdentity, AdapterThread as AdapterThreadSchema, FileActivityMetaKey, GitAttributionVersion, isBoundedToolValue, MaxSourceFailures, ToolIdentityMetaKey, type AdapterSourceFailure } from "@atape/domain" import { open, opendir, realpath, stat } from "node:fs/promises" import { homedir } from "node:os" import { codexHome as resolveCodexHome } from "@atape/adapter-catalog/node" @@ -19,7 +19,7 @@ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path" import { deflateRawSync, inflateRawSync } from "node:zlib" import { Effect, Option, Schema } from "effect" -const CursorVersion = 5 as const +const CursorVersion = 7 as const const MaxCursorBytes = 1024 * 1024 const MaxDecodedCursorBytes = 16 * 1024 * 1024 const CompressedCursorPrefix = "z1:" @@ -30,8 +30,8 @@ const MaxTitleScanBytes = 4 * 1024 * 1024 const MaxTitleCharacters = 80 const ReadBlockBytes = 64 * 1024 const MaxFilesPerSession = 100 -const EventProjectionVersion = 5 -const CanonicalProjectionRevisionOffset = 12 +const EventProjectionVersion = 7 +const CanonicalProjectionRevisionOffset = 14 const SourceOrderStride = 2 ** 39 // Keep the new coordinate domain beyond legacy four-digit RFC 3339 clocks. const SourceOrderBase = 2 ** 48 @@ -185,7 +185,7 @@ const ActiveCursorSchema = Schema.Struct({ const CursorSchema = Schema.Struct({ eventProjectionVersion: Schema.optionalKey(Schema.Number), usageVersion: Schema.optionalKey(Schema.Literal(1)), - v: Schema.Literal(CursorVersion), + v: Schema.Literals([5, 6, CursorVersion]), watermarkModifiedMs: Schema.Number, watermarkSessionId: Schema.String, commitSequence: Schema.optionalKey(Schema.Number), @@ -1432,16 +1432,23 @@ const mapItemUpdate = (item: Record, fallbackId: string): AcpSe } case "CommandExecution": { const command = itemText(item.command) || "Shell command" - return toolUpdate(id, command, "execute", item.status, item.exit_code) + const facts = nativeFileActivity(item) + return toolUpdate(id, command, "execute", item.status, item.exit_code, toolIdentity("CommandExecution"), facts?.rawInput, facts?.activity) } case "McpToolCall": { const label = [stringValue(item.server), stringValue(item.tool)].filter(Boolean).join("/") || "MCP tool" - return toolUpdate(id, label, "other", item.status) + const identity = mcpIdentity(item.server, item.tool) + return toolUpdate(id, label, "other", item.status, undefined, identity, + identity ? { server: item.server, tool: item.tool } : undefined) + } + case "FileChange": { + const facts = nativeFileActivity(item) + return toolUpdate(id, "Apply file changes", "edit", item.status, undefined, toolIdentity("FileChange"), facts?.rawInput, facts?.activity) + } + case "ImageView": { + const facts = nativeFileActivity(item) + return toolUpdate(id, "View image", "read", item.status, undefined, toolIdentity("ImageView"), facts?.rawInput, facts?.activity) } - case "FileChange": - return toolUpdate(id, "Apply file changes", "edit", item.status) - case "ImageView": - return toolUpdate(id, `View image ${basename(stringValue(item.path) ?? "image")}`, "read", item.status) case "Extension": return toolUpdate(id, stringValue(item.kind) || "Extension", "other", item.status) default: @@ -1501,7 +1508,7 @@ const mapResponseSupplementUpdate = ( } if (type === "custom_tool_call" || type === "function_call") { const label = stringValue(item.name) ?? "Codex tool" - return toolUpdate(id, label, responseToolKind(label), item.status) + return toolUpdate(id, label, responseToolKind(label), item.status, undefined, toolIdentity(item.name)) } return undefined } @@ -1516,12 +1523,58 @@ const responseToolKind = (label: string): "read" | "edit" | "search" | "execute" return "other" } +const isToolIdentity = Schema.is(AcpToolIdentity) +const toolIdentity = (name: unknown): AcpToolIdentity | undefined => { + const identity = { type: "tool", name } + return isToolIdentity(identity) ? identity : undefined +} +const mcpIdentity = (server: unknown, name: unknown): AcpToolIdentity | undefined => { + const identity = { type: "mcp", server, name } + return isToolIdentity(identity) ? identity : undefined +} + +const isFileActivity = Schema.is(AcpFileActivity) +const admittedFileActivity = (rawInput: unknown, entries: () => ReadonlyArray): { readonly rawInput?: unknown; readonly activity: AcpFileActivity | null } => { + // Copied paths retain their admitted original field at the upload privacy + // boundary. Omitted oversized inputs cannot justify another copy. + if (!isBoundedToolValue(rawInput)) return { activity: null } + const facts = [...new Map(entries().map(entry => [JSON.stringify(entry), entry])).values()] + return { rawInput, activity: isFileActivity(facts) ? facts : null } +} +function nativeFileActivity(item: Record): ReturnType | undefined { + if (item.type === "CommandExecution") { + if (!Array.isArray(item.parsed_cmd)) return { activity: null } + const parsed = item.parsed_cmd + return admittedFileActivity({ parsed_cmd: parsed }, () => parsed.flatMap(value => { + const command = record(value) + // This is the Provider's recorded classification; shell text does not + // establish a read, search scope or file path. + return command?.type === "read" || command?.type === "search" ? [{ operation: command.type, path: command.path }] : [] + })) + } + if (item.type === "FileChange") { + const changes = record(item.changes) + if (!changes) return { activity: null } + return admittedFileActivity({ changes: item.changes }, () => Object.entries(changes).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).flatMap(([path, value]) => { + const change = record(value) + if (!change || change.type !== "add" && change.type !== "update" && change.type !== "delete") return [{ operation: "invalid", path }] + return [{ operation: "edit", path }, ...(change.move_path == null ? [] : [{ operation: "edit", path: change.move_path }])] + })) + } + if (item.type === "ImageView") + return Object.hasOwn(item, "path") ? admittedFileActivity({ path: item.path }, () => [{ operation: "read", path: item.path }]) : { activity: null } + return undefined +} + const toolUpdate = ( id: string, title: string, kind: "read" | "edit" | "search" | "execute" | "fetch" | "other", providerStatus: unknown, - exitCode?: unknown + exitCode?: unknown, + identity?: AcpToolIdentity, + rawInput?: unknown, + activity?: AcpFileActivity | null ): AcpSessionUpdate => { const status = typeof exitCode === "number" ? exitCode === 0 ? "completed" as const : "failed" as const @@ -1531,7 +1584,9 @@ const toolUpdate = ( toolCallId: id, title: truncateUtf8(title, 500), kind, - ...(status === undefined ? {} : { status }) + ...(status === undefined ? {} : { status }), + ...(identity ? { _meta: { [ToolIdentityMetaKey]: identity, ...(activity === undefined ? {} : { [FileActivityMetaKey]: activity }) } } : {}), + ...(rawInput === undefined ? {} : { rawInput }) } } @@ -1903,7 +1958,10 @@ const decodeCursor = (value: string | null): CodexCursor => { : Buffer.from(value, "base64url") const parsed = JSON.parse(contents.toString("utf8")) as unknown const decoded = Schema.decodeUnknownOption(CursorSchema)(parsed) - if (Option.isSome(decoded) && validCursor(decoded.value)) return decoded.value + if (Option.isSome(decoded)) { + const migrated = { ...decoded.value, v: CursorVersion } + if (validCursor(migrated)) return migrated + } const previous = Schema.decodeUnknownOption(PreviousCursorSchema)(parsed) if (Option.isSome(previous)) { const { active, ...rest } = previous.value @@ -1954,7 +2012,7 @@ const validActive = (active: ActiveCursor) => nonNegative(active.revision) && ac (active.providerTitleUpdatedAt === undefined || sourceTimestamp(active.providerTitleUpdatedAt) !== null) && (active.sourceUpdatedAt === undefined || active.sourceUpdatedAt === null || sourceTimestamp(active.sourceUpdatedAt) !== null) && (active.frozenThreads === undefined || active.frozenThreads.length <= 100) -const validCursor = (cursor: CodexCursor) => (cursor.eventProjectionVersion === undefined || cursor.eventProjectionVersion === 2 || cursor.eventProjectionVersion === 3 || cursor.eventProjectionVersion === 4 || cursor.eventProjectionVersion === EventProjectionVersion) && Number.isFinite(cursor.watermarkModifiedMs) && +const validCursor = (cursor: CodexCursor) => (cursor.eventProjectionVersion === undefined || cursor.eventProjectionVersion === 2 || cursor.eventProjectionVersion === 3 || cursor.eventProjectionVersion === 4 || cursor.eventProjectionVersion === 5 || cursor.eventProjectionVersion === 6 || cursor.eventProjectionVersion === EventProjectionVersion) && Number.isFinite(cursor.watermarkModifiedMs) && cursor.watermarkModifiedMs >= 0 && cursor.watermarkSessionId.length <= 500 && (cursor.lastCanonicalSessionId === undefined || cursor.lastCanonicalSessionId.length <= 500) && (cursor.lastCanonicalSourceKey === undefined || /^[a-f0-9]{64}$/.test(cursor.lastCanonicalSourceKey)) && diff --git a/adapters/codex/src/index.ts b/adapters/codex/src/index.ts index 5966e3f..a99c00c 100644 --- a/adapters/codex/src/index.ts +++ b/adapters/codex/src/index.ts @@ -3,14 +3,14 @@ import type { AtapeAdapterModule, LegacyAdapterRuntime } from "@atape/domain" -import { CanonicalProfileVersion3, GitAttributionVersion } from "@atape/domain" +import { CanonicalProfileVersion5, GitAttributionVersion } from "@atape/domain" import { Effect, Ref, Semaphore } from "effect" import { CodexArchiveError, collectCodexPage, openCodexArchive } from "./codexArchive.ts" const module = { createAtapeAdapter: async (context: AdapterOpenContext & { readonly signal: AbortSignal }): Promise => { - if (!context.legacyCanonicalProfiles?.includes(CanonicalProfileVersion3)) { - throw new CodexArchiveError({ reason: "configuration", message: "Upgrade the ATape CLI to collect Codex with explicit source clock quality." }) + if (!context.legacyCanonicalProfiles?.includes(CanonicalProfileVersion5)) { + throw new CodexArchiveError({ reason: "configuration", message: "Upgrade the ATape CLI to collect Codex with explicit source clock quality and stable tool identities." }) } if (context.project.type === "git" && context.gitAttribution?.version !== GitAttributionVersion) { throw new CodexArchiveError({ reason: "configuration", message: "Upgrade the ATape CLI to collect Git Projects with shared attribution." }) @@ -22,7 +22,7 @@ const module = { const opening = yield* Semaphore.make(1) const lifetime = new AbortController() return { - canonicalProfileVersion: CanonicalProfileVersion3, + canonicalProfileVersion: CanonicalProfileVersion5, collect: request => { const signal = AbortSignal.any([context.signal, lifetime.signal, request.signal]) return Effect.runPromise(opening.withPermit(Effect.gen(function*() { diff --git a/adapters/cursor/package.json b/adapters/cursor/package.json index 5f97541..f08fbeb 100644 --- a/adapters/cursor/package.json +++ b/adapters/cursor/package.json @@ -1,6 +1,6 @@ { "name": "@atape/adapter-cursor", - "version": "0.5.11", + "version": "0.5.12", "type": "module", "description": "ATape Adapter for controlled Cursor sessions and native transcripts", "exports": { ".": "./dist/index.js" }, diff --git a/adapters/grok/package.json b/adapters/grok/package.json index 5014944..56051b2 100644 --- a/adapters/grok/package.json +++ b/adapters/grok/package.json @@ -1,6 +1,6 @@ { "name": "@atape/adapter-grok", - "version": "0.5.11", + "version": "0.5.12", "type": "module", "exports": { ".": "./dist/index.js" diff --git a/adapters/kimi/package.json b/adapters/kimi/package.json index 12e3be9..6d2d233 100644 --- a/adapters/kimi/package.json +++ b/adapters/kimi/package.json @@ -1,6 +1,6 @@ { "name": "@atape/adapter-kimi", - "version": "0.5.11", + "version": "0.5.12", "type": "module", "exports": { ".": "./dist/index.js" diff --git a/adapters/opencode/package.json b/adapters/opencode/package.json index f1bb273..acf8215 100644 --- a/adapters/opencode/package.json +++ b/adapters/opencode/package.json @@ -1,6 +1,6 @@ { "name": "@atape/adapter-opencode", - "version": "0.5.11", + "version": "0.5.12", "type": "module", "exports": { ".": "./dist/index.js" diff --git a/apps/cli/package.json b/apps/cli/package.json index 8e2c265..07a03f2 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,11 +1,11 @@ { "name": "@atape/cli", - "version": "0.5.11", + "version": "0.5.12", "description": "CLI and background Collector for ATape shared agent conversation history", "license": "MIT", "type": "module", "atapeRuntime": { - "releaseChannel": "legacy-bridge", + "releaseChannel": "target-owned", "updateTargetProtocol": "atape.update-target.v1", "targetReleaseCatalogProtocol": "atape.update-catalog.v3", "fromCaptureStateContracts": ["atape.client.v3-capture.v1", "atape.client.v3-capture.v2"], diff --git a/apps/cli/scripts/active-bridge-release-gate.mjs b/apps/cli/scripts/active-bridge-release-gate.mjs index 505335e..085026f 100644 --- a/apps/cli/scripts/active-bridge-release-gate.mjs +++ b/apps/cli/scripts/active-bridge-release-gate.mjs @@ -1,5 +1,5 @@ import { createHash } from "node:crypto" -import { mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises" +import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { fileURLToPath } from "node:url" @@ -12,24 +12,43 @@ const baselines = [ { name: "@atape/cli", filename: "cli-0.5.10.tgz", url: "https://registry.npmjs.org/@atape/cli/-/cli-0.5.10.tgz" }, { name: "@atape/adapter-codex", filename: "adapter-codex-0.5.10.tgz", url: "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.10.tgz" }, { name: "@atape/adapter-claude", filename: "adapter-claude-0.5.10.tgz", url: "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.10.tgz" } -] +].map(item => ({ ...item, version: "0.5.10", integrity: activeBridgeBaselineIntegrities[item.name] })) + +// This historical acceptance baseline is fixed, independently of npm latest, +// managed tags or the candidate release version. Retain all eight real archives. +const retainedBridgeVersion = "0.5.11" +const retainedBridgeIntegrities = { + "@atape/cli": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "@atape/adapter-codex": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "@atape/adapter-claude": "sha512-fgkgFrNltDa6+Vq/0+SE4xmqvCyPL1SdYgP0KIDu/uas2Oz5OyLly/562zzMkfKfymuFCZUGALqzMMbQ8BLw9Q==", + "@atape/adapter-opencode": "sha512-WD05HLDZyZg+24SAm9MAjk0Ob+tMoyuzI5Kk/teESNzWNX5aN7X8sCLAcls7NyaQg4uOmRfWAkclvKAR5CW4YQ==", + "@atape/adapter-codebuddy": "sha512-3BbggtxinR210yLzqk1GKaRxbT1aWnnv9Xe6/NWDvzkBh5lWnawva6azCGeF9gKjr7UjZgEUKxihzRgQ9eBqKA==", + "@atape/adapter-kimi": "sha512-y8iAqWqBT+I2E22ofgjVgATIFE9GrMGkCbpSpNw8UwtDx13GyUkijb0YmktcB0sQ7TWr8gmThL9wWeuLlKXacA==", + "@atape/adapter-grok": "sha512-sKY39aSPOt55XWtHZ6jrLZvcukIMdtfdfjqv+c0p3G1L6EqMZD6ai9mHigy1xcms17Xp/tB5BShPWrvw/PNICw==", + "@atape/adapter-cursor": "sha512-Ggc3vy7laTb2imRSjK6YvbLqKkSbF1Yl7I5v+yI5BxuwzBe4t8DnQg3Yqn9zs2oQi7X9SsxH7HhyrBqy2UAfSA==" +} +const retainedBridgePackages = Object.entries(retainedBridgeIntegrities).map(([name, integrity]) => ({ + name, integrity, version: retainedBridgeVersion, + filename: `${name.replace(/^@/, "").replaceAll("/", "-")}-${retainedBridgeVersion}.tgz`, + url: `https://registry.npmjs.org/${name}/-/${name.slice("@atape/".length)}-${retainedBridgeVersion}.tgz` +})) // A real external HTTP Seam. Only immutable, public, fixed-integrity historical // packages are accepted; callers cannot provide alternate URLs or hashes. -export async function acquireActiveBridgeBaselines(directory, fetchPackage = globalThis.fetch) { +async function acquirePinnedPackages(directory, packages, fetchPackage) { await mkdir(directory, { recursive: true, mode: 0o700 }) const acquired = [] try { - for (const item of baselines) { + for (const item of packages) { const signal = AbortSignal.timeout(deadlineMs) const response = await fetchPackage(item.url, { signal, redirect: "error", credentials: "omit" }) const reject = async message => { await response.body?.cancel().catch(() => {}); throw new Error(message) } - if (!response.ok) await reject(`Historical ${item.name}@0.5.10 download failed (${response.status})`) + if (!response.ok) await reject(`Historical ${item.name}@${item.version} download failed (${response.status})`) const declared = response.headers.get("content-length") if (declared !== null && (!Number.isSafeInteger(Number(declared)) || Number(declared) < 1 || Number(declared) > activeBridgeBaselineMaximumBytes)) { - await reject(`Historical ${item.name}@0.5.10 archive exceeds its size bound`) + await reject(`Historical ${item.name}@${item.version} archive exceeds its size bound`) } - if (response.body === null) throw new Error(`Historical ${item.name}@0.5.10 download has no body`) + if (response.body === null) throw new Error(`Historical ${item.name}@${item.version} download has no body`) const buffer = Buffer.alloc(activeBridgeBaselineMaximumBytes) let bytes = 0 const reader = response.body.getReader() @@ -39,47 +58,78 @@ export async function acquireActiveBridgeBaselines(directory, fetchPackage = glo const part = await reader.read() if (part.done) break const next = bytes + part.value.byteLength - if (next > activeBridgeBaselineMaximumBytes) throw new Error(`Historical ${item.name}@0.5.10 archive exceeds its size bound`) + if (next > activeBridgeBaselineMaximumBytes) throw new Error(`Historical ${item.name}@${item.version} archive exceeds its size bound`) buffer.set(part.value, bytes); bytes = next } } finally { await reader.cancel().catch(() => {}); reader.releaseLock() } const content = buffer.subarray(0, bytes) const integrity = `sha512-${createHash("sha512").update(content).digest("base64")}` - if (bytes === 0 || integrity !== activeBridgeBaselineIntegrities[item.name]) { - throw new Error(`Historical ${item.name}@0.5.10 archive does not match its fixed published integrity`) + if (bytes === 0 || integrity !== item.integrity) { + throw new Error(`Historical ${item.name}@${item.version} archive does not match its fixed published integrity`) } const file = join(directory, item.filename), temporary = `${file}.pending` await writeFile(temporary, content, { flag: "wx", mode: 0o600 }) await rename(temporary, file) - acquired.push(file) + acquired.push({ path: file, filename: item.filename, sha256: createHash("sha256").update(content).digest("hex") }) } - return { cli: acquired[0], codex: acquired[1], claude: acquired[2] } + return acquired } catch (cause) { // The caller owns this isolated directory. Never leave a half-acquired // baseline available for another gate to mistake for historical evidence. - await Promise.all(baselines.flatMap(item => [join(directory, item.filename), join(directory, `${item.filename}.pending`)]) + await Promise.all(packages.flatMap(item => [join(directory, item.filename), join(directory, `${item.filename}.pending`)]) .map(file => rm(file, { force: true }))) throw cause } } +export async function acquireActiveBridgeBaselines(directory, fetchPackage = globalThis.fetch) { + const acquired = await acquirePinnedPackages(directory, baselines, fetchPackage) + return { cli: acquired[0].path, codex: acquired[1].path, claude: acquired[2].path } +} + +export async function acquireRetainedActiveBridgeRelease(directory, fetchPackage = globalThis.fetch) { + const checksums = join(directory, "SHA256SUMS") + try { + const acquired = await acquirePinnedPackages(directory, retainedBridgePackages, fetchPackage) + const content = acquired.toSorted((left, right) => left.filename < right.filename ? -1 : left.filename > right.filename ? 1 : 0) + .map(item => `${item.sha256} ${item.filename}`).join("\n") + "\n" + await writeFile(`${checksums}.pending`, content, { flag: "wx", mode: 0o600 }) + await rename(`${checksums}.pending`, checksums) + return { version: retainedBridgeVersion, directory } + } catch (cause) { + await Promise.all([...retainedBridgePackages.flatMap(item => [join(directory, item.filename), join(directory, `${item.filename}.pending`)]), + checksums, `${checksums}.pending`].map(file => rm(file, { force: true }))) + throw cause + } +} + +export const assertRetainedActiveBridgeReleaseBytes = async bridge => { + if (bridge.version !== retainedBridgeVersion) throw new Error("Historical acceptance requires the genuine retained 0.5.11 bridge version") + for (const item of retainedBridgePackages) { + const content = await readFile(join(bridge.directory, item.filename)) + if (`sha512-${createHash("sha512").update(content).digest("base64")}` !== item.integrity) + throw new Error(`Retained ${item.name}@${item.version} archive does not match its fixed published integrity`) + } +} + // .11's bridge acceptance must use genuine .10 bytes. A later feature release // must explicitly supply retained bridge artifacts, never relabel feature bytes. -export async function verifyActiveBridgeRelease({ version, releaseChannel, releaseDirectory, bridgeRelease, featureRelease, faultPoint }, fetchPackage = globalThis.fetch) { +export async function verifyActiveBridgeRelease({ version, releaseChannel, releaseDirectory, bridgeRelease, featureRelease, faultPoint, baselineArchives }, fetchPackage = globalThis.fetch) { const parts = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(version) if (!parts) throw new Error("Release version must be SemVer") const [major, minor, patch] = parts.slice(1).map(Number) if (major === 0 && (minor < 5 || (minor === 5 && patch < 11))) return { required: false, reason: "older-release" } if (!["legacy-bridge", "target-owned"].includes(releaseChannel)) throw new Error("Active bridge acceptance requires an explicit release channel") - if (releaseChannel === "target-owned" && !bridgeRelease) throw new Error("Feature acceptance requires retained real bridge artifacts") if (process.platform === "win32") return { required: false, reason: "no-posix-managed-runtime" } + if (releaseChannel === "target-owned" && !bridgeRelease) throw new Error("Feature acceptance requires retained real bridge artifacts") const bridge = releaseChannel === "legacy-bridge" ? { version, directory: releaseDirectory } : bridgeRelease const feature = releaseChannel === "target-owned" ? { version, directory: releaseDirectory } : featureRelease if (faultPoint && !feature) throw new Error("Installed fault acceptance requires real feature artifacts") const root = await mkdtemp(join(tmpdir(), "atape-active-bridge-release-")) let failed = false try { - const baseline = await acquireActiveBridgeBaselines(join(root, "baseline"), fetchPackage) + const baseline = baselineArchives ?? await acquireActiveBridgeBaselines(join(root, "baseline"), fetchPackage) + if (releaseChannel === "target-owned") await assertRetainedActiveBridgeReleaseBytes(bridge) const evidence = await verifyActiveBridgeUpgrade(bridge.directory, join(root, "acceptance"), baseline.cli, baseline.codex, baseline.claude, bridge.version, feature ? { ...feature, ...(faultPoint ? { faultPoint } : {}) } : undefined) return { required: true, evidence } diff --git a/apps/cli/scripts/active-bridge-release-gate.node.mjs b/apps/cli/scripts/active-bridge-release-gate.node.mjs index 915d678..bd4d8f1 100644 --- a/apps/cli/scripts/active-bridge-release-gate.node.mjs +++ b/apps/cli/scripts/active-bridge-release-gate.node.mjs @@ -1,9 +1,9 @@ import assert from "node:assert/strict" -import { mkdtemp, readdir, rm } from "node:fs/promises" +import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { test } from "node:test" -import { acquireActiveBridgeBaselines, activeBridgeBaselineMaximumBytes, verifyActiveBridgeRelease } from "./active-bridge-release-gate.mjs" +import { acquireActiveBridgeBaselines, acquireRetainedActiveBridgeRelease, activeBridgeBaselineMaximumBytes, verifyActiveBridgeRelease } from "./active-bridge-release-gate.mjs" const fixture = async run => { const root = await mkdtemp(join(tmpdir(), "atape-baseline-gate-test-")) @@ -66,12 +66,81 @@ for (const version of ["0.5.11", "0.6.0", "1.0.0"]) { }) } -for (const releaseChannel of [undefined, "inferred", "target-owned"]) { +for (const releaseChannel of [undefined, "inferred"]) { test(`unsupported implicit bridge acceptance rejects channel ${releaseChannel}`, async () => { let calls = 0 await assert.rejects(verifyActiveBridgeRelease({ version: "0.5.12", releaseDirectory: "/unavailable", releaseChannel }, async () => { calls++; throw new Error("must not fetch") - }), releaseChannel === "target-owned" ? /retained real bridge artifacts/ : /explicit release channel/) + }), /explicit release channel/) assert.equal(calls, 0) }) } + +test("POSIX target-owned acceptance requires genuine retained bridge artifacts", { skip: process.platform === "win32" }, async () => { + let calls = 0 + await assert.rejects(verifyActiveBridgeRelease({ version: "0.5.12", releaseDirectory: "/unavailable", releaseChannel: "target-owned" }, async () => { + calls++; throw new Error("must not fetch") + }), /retained real bridge artifacts/) + assert.equal(calls, 0) +}) + +test("native Windows target-owned caller reports no POSIX runtime before acquiring bridge artifacts", { skip: process.platform !== "win32" }, async () => { + let calls = 0 + const result = await verifyActiveBridgeRelease({ version: "0.5.12", releaseDirectory: "/unavailable", releaseChannel: "target-owned" }, async () => { + calls++; throw new Error("must not fetch") + }) + assert.deepEqual(result, { required: false, reason: "no-posix-managed-runtime" }) + assert.equal(calls, 0) +}) + +test("retained bridge acquisition pins .11 public bytes independently of candidate and tags", async () => fixture(async root => { + let calls = 0 + await assert.rejects(acquireRetainedActiveBridgeRelease(root, async (url, options) => { + calls++ + assert.equal(url, "https://registry.npmjs.org/@atape/cli/-/cli-0.5.11.tgz") + assert.equal(options.redirect, "error"); assert.equal(options.credentials, "omit") + assert.ok(options.signal instanceof AbortSignal) + return new Response("a rebuilt bridge is not a historical npm archive") + }), /@atape\/cli@0\.5\.11 archive does not match its fixed published integrity/) + assert.equal(calls, 1) + assert.deepEqual(await readdir(root), []) +})) + +test("unavailable retained bridge bytes fail closed without a synthetic feature descriptor", async () => fixture(async root => { + let cancelled = false + await assert.rejects(acquireRetainedActiveBridgeRelease(root, async () => new Response(new ReadableStream({ + cancel() { cancelled = true } + }), { status: 404 })), /@atape\/cli@0\.5\.11 download failed \(404\)/) + assert.equal(cancelled, true) + assert.deepEqual(await readdir(root), []) +})) + +test("retained bridge acquisition preserves the bounded stream contract", async () => fixture(async root => { + let cancelled = false + await assert.rejects(acquireRetainedActiveBridgeRelease(root, async () => new Response(new ReadableStream({ + start(controller) { controller.enqueue(new Uint8Array(activeBridgeBaselineMaximumBytes + 1)) }, + cancel() { cancelled = true } + }))), /size bound/) + assert.equal(cancelled, true) + assert.deepEqual(await readdir(root), []) +})) + +test("reusing retained bridge files rejects relabeling and rechecks published integrity before every installed case", async () => fixture(async root => { + if (process.platform === "win32") return + await assert.rejects(verifyActiveBridgeRelease({ version: "0.5.12", releaseChannel: "target-owned", releaseDirectory: "/unavailable", + bridgeRelease: { version: "0.5.12", directory: root }, baselineArchives: { cli: "/unavailable", codex: "/unavailable", claude: "/unavailable" } + }, async () => { throw new Error("Reused archives must not be downloaded again") }), /genuine retained 0\.5\.11 bridge version/) + await writeFile(join(root, "atape-cli-0.5.11.tgz"), "changed after acquisition") + await assert.rejects(verifyActiveBridgeRelease({ version: "0.5.12", releaseChannel: "target-owned", releaseDirectory: "/unavailable", + bridgeRelease: { version: "0.5.11", directory: root }, baselineArchives: { cli: "/unavailable", codex: "/unavailable", claude: "/unavailable" } + }, async () => { throw new Error("Reused archives must not be downloaded again") }), /Retained @atape\/cli@0\.5\.11 archive does not match/) +})) + +test("reusing .10 archives cannot bypass the installed caller's genuine historical integrity proof", async () => fixture(async root => { + if (process.platform === "win32") return + const cli = join(root, "cli.tgz") + await writeFile(cli, "a substituted historical archive") + await assert.rejects(verifyActiveBridgeRelease({ version: "0.5.12", releaseChannel: "legacy-bridge", releaseDirectory: "/unavailable", + baselineArchives: { cli, codex: "/unavailable", claude: "/unavailable" } + }, async () => { throw new Error("Reused archives must not be downloaded again") }), /Requires the exact npm-published @atape\/cli@0\.5\.10 archive/) +})) diff --git a/apps/cli/scripts/codex-clock-release-gate.mjs b/apps/cli/scripts/codex-clock-release-gate.mjs index 58db970..e56250b 100644 --- a/apps/cli/scripts/codex-clock-release-gate.mjs +++ b/apps/cli/scripts/codex-clock-release-gate.mjs @@ -64,7 +64,7 @@ export async function acquireCodexClockBaselines(directory, fetchPackage = globa // Every release at or beyond the first clock-profile release must exercise the // genuine old parent transition on POSIX; no environment skip override exists. -export async function verifyCodexClockRelease({ version, releaseDirectory }, fetchPackage = globalThis.fetch) { +export async function verifyCodexClockRelease({ version, releaseDirectory, releaseChannel, bridgeRelease }, fetchPackage = globalThis.fetch) { const parts = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(version) if (!parts) throw new Error("Release version must be SemVer") const [major, minor, patch] = parts.slice(1).map(Number) @@ -73,13 +73,23 @@ export async function verifyCodexClockRelease({ version, releaseDirectory }, fet const root = await mkdtemp(join(tmpdir(), "atape-codex-clock-release-")) try { const baseline = await acquireCodexClockBaselines(join(root, "baseline"), fetchPackage) - const evidence = await verifyCodexClockUpgrade(releaseDirectory, join(root, "acceptance"), baseline.cli, baseline.codex, version) + let evidence + if (releaseChannel === "target-owned") { + const { acquireRetainedActiveBridgeRelease, assertRetainedActiveBridgeReleaseBytes } = await import("./active-bridge-release-gate.mjs") + const bridge = bridgeRelease ?? await acquireRetainedActiveBridgeRelease(join(root, "retained-bridge"), fetchPackage) + await assertRetainedActiveBridgeReleaseBytes(bridge) + evidence = await verifyCodexClockUpgrade(bridge.directory, join(root, "acceptance"), baseline.cli, baseline.codex, bridge.version, + { directory: releaseDirectory, version }) + } else { + evidence = await verifyCodexClockUpgrade(releaseDirectory, join(root, "acceptance"), baseline.cli, baseline.codex, version) + } return { required: true, evidence } } finally { await rm(root, { recursive: true, force: true }) } } if (process.argv[1] === fileURLToPath(import.meta.url)) { const release = await loadReleaseContract(fileURLToPath(new URL("../../..", import.meta.url))) - const result = await verifyCodexClockRelease({ version: release.version, releaseDirectory: process.argv[2] ?? release.releaseDirectory }) + const result = await verifyCodexClockRelease({ version: release.version, releaseChannel: release.releaseChannel, + releaseDirectory: process.argv[2] ?? release.releaseDirectory }) process.stdout.write(`Codex clock release gate: ${JSON.stringify(result)}\n`) } diff --git a/apps/cli/scripts/codex-clock-release-gate.node.mjs b/apps/cli/scripts/codex-clock-release-gate.node.mjs index 5857d65..75c9417 100644 --- a/apps/cli/scripts/codex-clock-release-gate.node.mjs +++ b/apps/cli/scripts/codex-clock-release-gate.node.mjs @@ -65,3 +65,16 @@ for (const version of ["0.5.10", "0.5.11", "0.6.0", "1.0.0"]) { assert.equal(calls, 1) }) } + +test("target-owned clock acceptance cannot skip the genuine .9 boundary when bridge artifacts are supplied", { skip: process.platform === "win32" }, async () => { + let calls = 0 + await assert.rejects(verifyCodexClockRelease({ version: "0.5.12", releaseChannel: "target-owned", releaseDirectory: "/unavailable", + bridgeRelease: { version: "0.5.11", directory: "/unavailable" } + }, async (url, options) => { + calls++ + assert.equal(url,"https://registry.npmjs.org/@atape/cli/-/cli-0.5.9.tgz") + assert.equal(options.credentials,"omit");assert.equal(options.redirect,"error") + return new Response("historical clock parent unavailable",{status:503}) + }), /Historical @atape\/cli@0\.5\.9 download failed \(503\)/) + assert.equal(calls,1) +}) diff --git a/apps/cli/scripts/verify-active-bridge-upgrade.mjs b/apps/cli/scripts/verify-active-bridge-upgrade.mjs index 6aea4ad..38c593e 100644 --- a/apps/cli/scripts/verify-active-bridge-upgrade.mjs +++ b/apps/cli/scripts/verify-active-bridge-upgrade.mjs @@ -3,6 +3,7 @@ import { execFile } from "node:child_process" import { createHash, randomUUID } from "node:crypto" import { appendFile, chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, writeFile } from "node:fs/promises" import { basename, dirname, join } from "node:path" +import { DatabaseSync } from "node:sqlite" import { fileURLToPath, pathToFileURL } from "node:url" import { promisify } from "node:util" import { build } from "esbuild" @@ -21,6 +22,10 @@ const integrity = bytes => `sha512-${createHash("sha512").update(bytes).digest(" const json = async file => JSON.parse(await readFile(file, "utf8")) const missing = cause => cause.code === "ENOENT" const optionalBytes = file => readFile(file).catch(cause => { if (missing(cause)) return undefined; throw cause }) +const journalVersion = path => { + const db = new DatabaseSync(path, { readOnly: true }) + try { return Number(db.prepare("PRAGMA user_version").get().user_version) } finally { db.close() } +} const alive = pid => { try { process.kill(pid, 0); return true } catch (cause) { if (cause.code === "ESRCH") return false; throw cause } } const waitFor = async (check, description, timeout = 120_000) => { const deadline = Date.now() + timeout @@ -97,7 +102,8 @@ export async function verifyActiveBridgeUpgrade(releaseDirectory, fixtureDirecto // A late-offline .10 sees the newer feature GitHub release from the outset, // while immutable legacy routes and npm latest keep pointing to the bridge. const distribution = { bundle, legacyBundle, catalog, legacyCatalog, releaseBody, packages: publicPackages, feature, - clockOffsetMs: 0, serverVersion: "0.5.10", minimumCLIVersion: "0.5.10", ...(faultPoint ? { fault: { point: faultPoint } } : {}) } + clockOffsetMs: 0, serverVersion: feature?.version ?? "0.5.10", minimumCLIVersion: feature?.version ?? "0.5.10", + ...(faultPoint ? { fault: { point: faultPoint } } : {}) } await writeFile(publicRelease, JSON.stringify(distribution)) const prefix = join(root, "prefix"), home = join(root, "home"), userHome = join(root, "user") const globalRoot = join(prefix, "lib", "node_modules"), bootstrap = join(globalRoot, "@atape", "cli", "dist", "atape.js") @@ -133,7 +139,8 @@ globalThis.fetch=async (input,init)=> { const address=String(input); if(address.startsWith("data:"))return realFetch(input,init); const fixture=JSON.parse(readFileSync(${JSON.stringify(publicRelease)},"utf8")); - appendFileSync(${JSON.stringify(requests)},JSON.stringify({address,pid:process.pid})+"\n"); + appendFileSync(${JSON.stringify(requests)},JSON.stringify({address,pid:process.pid, + ...(address==="https://upgrade-fixture.invalid/api/v1/instance"?{serverVersion:fixture.serverVersion,minimumCLIVersion:fixture.minimumCLIVersion}:{})})+"\n"); for (const protocol of ["atape.release-bundle.v2","atape.release-bundle.v1"]) { const migration=protocol.endsWith("v2"); const catalogURL=migration?${JSON.stringify(releaseCatalogURL("atape.update-catalog.v2"))}:${JSON.stringify(releaseCatalogURL("atape.update-catalog.v1"))}; @@ -384,6 +391,20 @@ else if(args[0]==="install") { assert.equal(JSON.parse(Buffer.from(before.state.checkpoint.cursor, "base64url").toString("utf8")).eventProjectionVersion, 5) assert.equal(before.state.checkpoint.rawObjects.length, 1) assert.ok(before.state.checkpoint.rawObjects[0].serverOffset > 0) + assert.equal(before.status.jobs.find(job => job.adapterId === "claude")?.state, "healthy", + "The actual baseline Claude SourceCapture cycle must open its journal before empty discovery") + const journalKey = sha256(JSON.stringify(["https://upgrade-fixture.invalid", "upgrade-user"])) + const journalPath = join(home, "state", "collector.json.captures", `${journalKey}.sqlite`) + const journalBinding = { instanceOrigin: "https://upgrade-fixture.invalid", userId: "upgrade-user", installationId: before.state.installationId } + const installation = await json(join(home, "state", "collector.json.capture-installation.json")) + assert.equal(installation.protocol, "atape.capture-installation.v1"); assert.equal(installation.phase, "ready") + assert.equal(installation.installationId, journalBinding.installationId) + assert.deepEqual(installation.accounts, [{ key: journalKey, phase: "ready" }]) + assert.deepEqual(await json(join(dirname(journalPath), `${journalKey}.binding.json`)), + { protocol: "atape.capture-account.v1", ...journalBinding, phase: "ready" }) + assert.equal((await lstat(journalPath)).isFile(), true) + const baselineJournalVersion = journalVersion(journalPath) + assert.ok([7, 8].includes(baselineJournalVersion), "The genuine baseline must create a supported registered journal") const oldBatches = await captured(captures), oldEvents = oldBatches.flatMap(batch => batch.events) assert.equal(oldEvents.length, 4) assert.ok(oldBatches.every(batch => batch.canonicalProfileVersion === "atape.acp-centered.v3")) @@ -392,7 +413,7 @@ else if(args[0]==="install") { assert.equal(oldTool._meta, undefined); assert.equal(oldTool.rawInput, undefined) const credentials = new Map(await Promise.all((await credentialFiles()).map(async file => [file, await readFile(file)]))) const rawBefore = await readFile(rawCaptures) - distribution.serverVersion = version; distribution.minimumCLIVersion = version + distribution.serverVersion = feature?.version ?? version; distribution.minimumCLIVersion = feature?.version ?? version await writeFile(publicRelease, JSON.stringify(distribution)) await invoke("enable-auto") await invoke("dispatch") @@ -435,6 +456,8 @@ else if(args[0]==="install") { if (feature) { const preceding = await captured(captures), previousEvents = preceding.flatMap(batch => batch.events) const previousRaw = await readFile(rawCaptures), previousRawObjects = bridgeContinued.state.checkpoint.rawObjects + const previousJournalVersion = journalVersion(journalPath) + assert.ok([7, 8].includes(previousJournalVersion)) // Advance only the owned clock Seam. Do not edit the updater's private schedule // or force/manual-update APIs to manufacture an automatic second leg. distribution.clockOffsetMs += 2 * 24 * 60 * 60 * 1000 @@ -443,6 +466,31 @@ else if(args[0]==="install") { await invoke("dispatch") const recoveryEvidence = faultPoint ? await interruptAndRecoverFeature() : undefined const featureState = await waitSelected(feature.version), featureEntryHash = await selectedHash(feature.version, feature.packages) + const targetControl = await json(join(home, "updates", "control.json")) + assert.equal(targetControl.phase, "completed"); assert.equal(targetControl.forwardOnly, true) + assert.equal(targetControl.target.version, feature.version) + const migration = await json(join(home, "updates", "capture-migration.json")) + const requirement = migration.required.requirement, receipt = migration.receipt + assert.equal(migration.protocol, "atape.capture-migration.v1"); assert.equal(migration.phase, "completed") + assert.equal(migration.attempt, undefined) + assert.deepEqual(migration.required, await json(join(home, "updates", "capture-migration.required.json"))) + assert.equal(requirement.outerKey, targetControl.key); assert.equal(requirement.source.version, version) + assert.deepEqual(requirement.target, targetControl.target); assert.deepEqual(requirement.bundle.packages, feature.bundle.packages) + assert.deepEqual(requirement.inventory.map(account => account.key), [journalKey]) + const account = requirement.inventory[0] + assert.equal(account.path, journalPath); assert.deepEqual(account.binding, journalBinding) + assert.equal(account.installationId, journalBinding.installationId); assert.equal(account.format, previousJournalVersion) + assert.equal(account.deferred, false) + assert.equal(receipt.protocol, "atape.capture-migration-receipt.v1"); assert.equal(receipt.planId, "journal-v7-to-v8") + assert.equal(receipt.outerKey, targetControl.key); assert.equal(receipt.requirementHash, requirement.hash) + assert.equal(receipt.bundleFingerprint, requirement.bundleFingerprint) + assert.deepEqual(receipt.target, { version: feature.version, captureStateContract: feature.bundle.captureStateContract }) + assert.deepEqual(receipt.accounts, [{ key: journalKey, bindingHash: account.bindingHash, result: "v8" }]) + assert.deepEqual(migration.progress, receipt.accounts); assert.equal(journalVersion(journalPath), 8) + const targetOwner = await json(join(home, "updates", "target-owner.json")) + assert.equal(targetOwner.protocol, "atape.update-target.v1"); assert.deepEqual(targetOwner.bundle, feature.bundle) + assert.equal(targetOwner.entrySha256, featureEntryHash); assert.equal(targetOwner.bootstrapEntry, bootstrap) + assert.equal(targetOwner.paths.atapeHome, home) await waitFor(async () => (await captured(captures)).slice(preceding.length).some(batch => batch.canonicalProfileVersion === "atape.acp-centered.v5"), "Installed feature Collector did not replay native history to v5") const replay = (await captured(captures)).slice(preceding.length) @@ -469,6 +517,12 @@ else if(args[0]==="install") { assert.ok(targets.every(entry => entry.sha256 === featureEntryHash), "Target preparation and activation must use the immutable feature executable") assert.equal((await entries()).find(entry => entry.args[0] === "__collector-daemon" && entry.pid === featureState.status.pid)?.sha256, featureEntryHash) const requested = await captured(requests) + const instanceReadbacks = requested.filter(request => request.address === "https://upgrade-fixture.invalid/api/v1/instance") + assert.ok(instanceReadbacks.some(request => request.pid === originalPid || oldWorkers.some(worker => worker.pid === request.pid)), + "The genuine .10 caller must observe the already-upgraded Server") + assert.ok(instanceReadbacks.length > 0 && instanceReadbacks.every(request => + request.serverVersion === feature.version && request.minimumCLIVersion === feature.version), + "All installed callers must see the future Server version and minimum CLI from the first leg onward") // The genuine old Collector may discover and cache the immutable v2 // descriptor before dispatch; its copied worker then consumes that cache. const oldCollector = (await entries()).find(entry => entry.args[0] === "__collector-daemon" && entry.pid === originalPid) @@ -482,9 +536,12 @@ else if(args[0]==="install") { assert.equal(await optionalBytes(join(home, "updates", "target-update.json")), undefined) featureEvidence = { version: feature.version, entrySha256: featureEntryHash, cliIntegrity: feature.packages["@atape/cli"].manifest.dist.integrity, bundle: feature.bundle, actualBridgeAutomaticWorker: true, actualTargetPrepareAndActivate: true, + nonEmptyCaptureMigrationReceiptVerified: true, registeredCaptureAccountsVerified: receipt.accounts.length, + sourceJournalVersion: previousJournalVersion, targetJournalVersion: 8, targetOwnerVerified: true, nativeV5Replay: true, eventIDsPreserved: true, revisionsAdvanced: true, rawReceiptsPreservedAcrossReplay: true, nativeFileActivityReplayed: true, appendedContinuation: true, lateOfflineLegacyRoutesRetained: true, retainedOldBootstrapRejectedUnknownCaptureLedger: true, globalBootstrapBytesPreserved: true, + serverUpgradedBeforeLegacyClient: true, serverVersionReadbacks: instanceReadbacks.length, ...(recoveryEvidence ? { recovery: recoveryEvidence } : {}) } } await unchanged(credentials) @@ -531,6 +588,7 @@ else if(args[0]==="install") { credentialBytesPreserved: true, acknowledgedCaptureProgressPreserved: true, canonicalBridgeNoReplay: true, rawReceiptsPreserved: true, appendedContinuation: true, stopAndUpdatesOffPreserved: true, retainedOldBootstrapRejectedUnknownCaptureLedger: true, + actualBaselineClaudeJournalRegistered: true, baselineJournalVersion, fullChain: feature ? "passed-with-controlled-installed-candidate-artifacts" : "not-tested-feature-artifacts-not-supplied", ...(featureEvidence ? { feature: featureEvidence } : {}), providerCaptureTested: "constructed native-format Codex source through genuine installed readers; actual Claude packages installed and admitted, Claude live source not exercised", diff --git a/apps/cli/scripts/verify-codex-clock-upgrade-terminal.py b/apps/cli/scripts/verify-codex-clock-upgrade-terminal.py index 24d67d0..fe83962 100644 --- a/apps/cli/scripts/verify-codex-clock-upgrade-terminal.py +++ b/apps/cli/scripts/verify-codex-clock-upgrade-terminal.py @@ -104,7 +104,8 @@ def send(keys): assert process.poll() == 0, "Console did not exit: " + transcript[-8000:].decode(errors="replace") assert termios.tcgetattr(slave) == before, "terminal attributes were not restored" print(json.dumps({"phase": phase, "terminalRestored": True, "exit": process.returncode, - "rejectionVisible": phase == "reject", "versionShown": version if phase == "rebound" else "0.5.9"})) + "rejectionVisible": phase == "reject", + "versionShown": "0.5.9" if phase == "reject" else version})) finally: if process.poll() is None: os.killpg(process.pid, signal.SIGKILL) diff --git a/apps/cli/scripts/verify-codex-clock-upgrade.mjs b/apps/cli/scripts/verify-codex-clock-upgrade.mjs index d92e471..d4daa7e 100644 --- a/apps/cli/scripts/verify-codex-clock-upgrade.mjs +++ b/apps/cli/scripts/verify-codex-clock-upgrade.mjs @@ -2,12 +2,13 @@ import assert from "node:assert/strict" import { execFile } from "node:child_process" import { createHash } from "node:crypto" import { appendFile, chmod, mkdir, readFile, readdir, realpath, writeFile } from "node:fs/promises" -import { dirname, join } from "node:path" +import { basename, dirname, join } from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" import { promisify } from "node:util" import { build } from "esbuild" import { AdapterCollectionLimits } from "../../../packages/domain/src/collector.ts" -import { releaseBundleURL, releaseCatalogURL, mergeMigrationReleaseBundle, mergeReleaseBundle, migrationReleaseBundleSection, releaseBundleSection } from "../../../packages/domain/src/releaseCatalog.ts" +import { releaseBundleURL, releaseCatalogURL, mergeMigrationReleaseBundle, mergeReleaseBundle, migrationReleaseBundleSection, releaseBundleSection, + mergeTargetReleaseBundle, targetReleaseBundleSection } from "../../../packages/domain/src/releaseCatalog.ts" const execute = promisify(execFile) const packages = ["@atape/cli", "@atape/adapter-codex", "@atape/adapter-claude", "@atape/adapter-opencode", @@ -34,7 +35,7 @@ const item = (id, type, text, timestamp) => ({ timestamp, type: "event_msg", pay /** Actual published old parent and exact candidate npm archives, in a fresh HOME. * Test Adapters replace only remote distribution/Server HTTP and OS registration; * the real CLI, updater, source reader, Collector and state Interfaces execute. */ -export async function verifyCodexClockUpgrade(releaseDirectory, fixtureDirectory, baselineCLI, baselineCodex, version) { +export async function verifyCodexClockUpgrade(releaseDirectory, fixtureDirectory, baselineCLI, baselineCodex, version, featureRelease) { for (const [name, path] of [["@atape/cli", baselineCLI], ["@atape/adapter-codex", baselineCodex]]) { assert.equal(integrity(await readFile(path)), codexClockBaselineIntegrities[name], `Requires the exact npm-published ${name}@0.5.9 archive`) } @@ -42,7 +43,8 @@ export async function verifyCodexClockUpgrade(releaseDirectory, fixtureDirectory const root = await realpath(fixtureDirectory) const realNpm = (await execute("which", ["npm"], { encoding: "utf8" })).stdout.trim() assert.ok(realNpm.startsWith("/")) - const checksumLines = (await readFile(join(releaseDirectory,"SHA256SUMS"),"utf8")).trim().split("\n") + const loadPackages = async (directory, releaseVersion) => { + const checksumLines = (await readFile(join(directory,"SHA256SUMS"),"utf8")).trim().split("\n") const checksums = new Map(checksumLines.map(line => { const match=/^([a-f0-9]{64}) ([A-Za-z0-9.-]+\.tgz)$/.exec(line) assert.ok(match,"Release checksums must identify bounded archive filenames") @@ -52,18 +54,22 @@ export async function verifyCodexClockUpgrade(releaseDirectory, fixtureDirectory assert.equal(checksumLines.length,packages.length) const publicPackages = {} for (const name of packages) { - const archive = await realpath(join(releaseDirectory, `${name.replace(/^@/, "").replaceAll("/", "-")}-${version}.tgz`)) + const archive = await realpath(join(directory, `${name.replace(/^@/, "").replaceAll("/", "-")}-${releaseVersion}.tgz`)) const manifest = JSON.parse((await execute("tar", ["-xOf", archive, "package/package.json"], { encoding: "utf8", maxBuffer: 256 * 1024 })).stdout) - assert.equal(manifest.name, name); assert.equal(manifest.version, version) + assert.equal(manifest.name, name); assert.equal(manifest.version, releaseVersion) const bytes = await readFile(archive) - assert.equal(sha256(bytes),checksums.get(`${name.replace(/^@/, "").replaceAll("/", "-")}-${version}.tgz`),"Clock transition requires the exact checksummed candidate archives") + assert.equal(sha256(bytes),checksums.get(`${name.replace(/^@/, "").replaceAll("/", "-")}-${releaseVersion}.tgz`),"Clock transition requires the exact checksummed candidate archives") publicPackages[name] = { archive, manifest: { ...manifest, dist: { integrity: integrity(bytes), - tarball: `https://registry.npmjs.org/${name}/-/${name.slice("@atape/".length)}-${version}.tgz` } } } + tarball: `https://registry.npmjs.org/${name}/-/${name.slice("@atape/".length)}-${releaseVersion}.tgz` } } } } + return publicPackages + } + const publicPackages = await loadPackages(releaseDirectory, version) const candidateProfile = publicPackages["@atape/adapter-codex"].manifest.atapeAdapter.legacyCanonicalProfile const cliManifest = publicPackages["@atape/cli"].manifest const channel = cliManifest.atapeRuntime.releaseChannel assert.ok(["legacy-bridge", "target-owned"].includes(channel), "Clock transition requires an explicit packaged publication channel") + assert.equal(channel, "legacy-bridge", "A target-owned clock candidate requires the genuine retained bridge leg; it cannot publish a legacy descriptor") assert.equal(candidateProfile, channel === "legacy-bridge" ? "atape.acp-centered.v3" : "atape.acp-centered.v5", "Clock projection must match the actual release channel") const candidateProjection = channel === "legacy-bridge" ? 5 : 7 const bundle = { protocol: "atape.release-bundle.v2", version, captureStateContract: cliManifest.atapeRuntime.stateContract, @@ -71,9 +77,30 @@ export async function verifyCodexClockUpgrade(releaseDirectory, fixtureDirectory packages: packages.map(name => ({ name, ...publicPackages[name].manifest.dist })) } const legacyBundle = { ...bundle, protocol: "atape.release-bundle.v1" }; delete legacyBundle.migration const catalog = mergeMigrationReleaseBundle(undefined, bundle), legacyCatalog = mergeReleaseBundle(undefined, legacyBundle) - const releaseBody = migrationReleaseBundleSection(bundle) + "\n" + releaseBundleSection(legacyBundle) + let releaseBody = migrationReleaseBundleSection(bundle) + "\n" + releaseBundleSection(legacyBundle) + let feature, bridgeTarget + if (featureRelease) { + assert.equal(version, "0.5.11", "The historical clock bridge must be the retained published .11 release") + assert.equal(cliManifest.atapeRuntime.updateTargetProtocol, "atape.update-target.v1") + bridgeTarget = { protocol: "atape.release-bundle.v3", version, + captureStateContract: cliManifest.atapeRuntime.stateContract, updateControlProtocol: cliManifest.atapeRuntime.updateControlProtocol, + updateTargetProtocol: cliManifest.atapeRuntime.updateTargetProtocol, + fromCaptureStateContracts: cliManifest.atapeRuntime.fromCaptureStateContracts, packages: bundle.packages } + releaseBody += "\n" + targetReleaseBundleSection(bridgeTarget) + const artifacts = await loadPackages(featureRelease.directory, featureRelease.version), manifest = artifacts["@atape/cli"].manifest + assert.equal(manifest.atapeRuntime.releaseChannel, "target-owned") + assert.equal(manifest.atapeRuntime.updateTargetProtocol, "atape.update-target.v1") + assert.equal(artifacts["@atape/adapter-codex"].manifest.atapeAdapter.legacyCanonicalProfile, "atape.acp-centered.v5") + const target = { protocol: "atape.release-bundle.v3", version: featureRelease.version, + captureStateContract: manifest.atapeRuntime.stateContract, updateControlProtocol: manifest.atapeRuntime.updateControlProtocol, + updateTargetProtocol: manifest.atapeRuntime.updateTargetProtocol, fromCaptureStateContracts: manifest.atapeRuntime.fromCaptureStateContracts, + packages: packages.map(name => ({ name, ...artifacts[name].manifest.dist })) } + feature = { version: featureRelease.version, bundle: target, packages: artifacts, releaseBody: targetReleaseBundleSection(target), + catalog: mergeTargetReleaseBundle(mergeTargetReleaseBundle(undefined, bridgeTarget), target) } + } const publicRelease = join(root, "public-release.json") - const distribution = { bundle, legacyBundle, catalog, legacyCatalog, releaseBody, packages: publicPackages } + const distribution = { bundle, legacyBundle, catalog, legacyCatalog, releaseBody, packages: publicPackages, feature, clockOffsetMs: 0, + ...(bridgeTarget ? { bridgeTarget, targetCatalog: mergeTargetReleaseBundle(undefined, bridgeTarget), featureAdvertised: false } : {}) } await writeFile(publicRelease, JSON.stringify({ ...distribution, serverVersion:"0.5.9", minimumCLIVersion:"0.5.9" })) const prefix = join(root, "prefix"), home = join(root, "home"), userHome = join(root, "user") const globalRoot = join(prefix, "lib", "node_modules"), bootstrap = join(globalRoot, "@atape", "cli", "dist", "atape.js") @@ -92,13 +119,15 @@ for (const method of ["execFile", "execFileSync", "spawn", "spawnSync"]) { const original=childProcess[method]; childProcess[method]=function(file,...args) { if (["launchctl","systemctl","open","xdg-open","rundll32"].includes(basename(file))) { - appendFileSync(${JSON.stringify(native)},JSON.stringify({method,file})+"\n"); + appendFileSync(${JSON.stringify(native)},JSON.stringify({method,file,args:args[0],rejected:true})+"\n"); throw new Error("Native OS registration/browser commands are forbidden in isolated transition acceptance"); } return Reflect.apply(original,this,[file,...args]); }; } syncBuiltinESMExports(); +const realNow=Date.now; +Date.now=()=>realNow()+(JSON.parse(readFileSync(${JSON.stringify(publicRelease)},"utf8")).clockOffsetMs??0); const realFetch = globalThis.fetch; const saved = file => existsSync(file) ? readFileSync(file,"utf8").split("\n").filter(Boolean).map(JSON.parse) : []; globalThis.fetch=async (input,init)=> { @@ -106,7 +135,11 @@ globalThis.fetch=async (input,init)=> { if(address.startsWith("data:"))return realFetch(input,init); const fixture=JSON.parse(readFileSync(${JSON.stringify(publicRelease)},"utf8")); appendFileSync(${JSON.stringify(requests)},JSON.stringify({address,pid:process.pid})+"\n"); - if(address===${JSON.stringify(releaseCatalogURL("atape.update-catalog.v3"))}||address===${JSON.stringify(releaseBundleURL(version,"atape.release-bundle.v3"))})return new Response(null,{status:404}); + if(address===${JSON.stringify(releaseCatalogURL("atape.update-catalog.v3"))})return fixture.bridgeTarget? + Response.json(fixture.featureAdvertised?fixture.feature.catalog:fixture.targetCatalog):new Response(null,{status:404}); + if(address===${JSON.stringify(releaseBundleURL(version,"atape.release-bundle.v3"))})return fixture.bridgeTarget?Response.json(fixture.bridgeTarget):new Response(null,{status:404}); + if(fixture.feature&&address===${JSON.stringify(feature && releaseBundleURL(feature.version,"atape.release-bundle.v3"))})return fixture.featureAdvertised?Response.json(fixture.feature.bundle):new Response(null,{status:404}); + if(fixture.feature&&${JSON.stringify(feature ? [releaseBundleURL(feature.version,"atape.release-bundle.v1"),releaseBundleURL(feature.version,"atape.release-bundle.v2")] : [])}.includes(address))return new Response(null,{status:404}); for (const protocol of ["atape.release-bundle.v2","atape.release-bundle.v1"]) { const migration=protocol.endsWith("v2"); const catalogURL=migration?${JSON.stringify(releaseCatalogURL("atape.update-catalog.v2"))}:${JSON.stringify(releaseCatalogURL("atape.update-catalog.v1"))}; @@ -116,11 +149,15 @@ globalThis.fetch=async (input,init)=> { } if(address==="https://api.github.com/repos/SingleMai/ATape/releases/latest")return Response.json({tag_name:${JSON.stringify(`v${version}`)},prerelease:false,draft:false,published_at:"2026-10-12T00:00:00Z"}); if(address===${JSON.stringify(`https://api.github.com/repos/SingleMai/ATape/releases/tags/v${version}`)})return Response.json({tag_name:${JSON.stringify(`v${version}`)},prerelease:false,draft:false,published_at:"2026-10-12T00:00:00Z",body:fixture.releaseBody}); - const artifact=Object.values(fixture.packages).find(item=>item.manifest.dist.tarball===address); + if(fixture.feature&&address==="https://api.github.com/repos/SingleMai/ATape/releases/tags/v"+fixture.feature.version)return Response.json({tag_name:"v"+fixture.feature.version,prerelease:false,draft:false,published_at:"2026-10-12T00:00:00Z",body:fixture.feature.releaseBody}); + const allPackages=[...Object.values(fixture.packages),...Object.values(fixture.feature?.packages??{})]; + const artifact=allPackages.find(item=>item.manifest.dist.tarball===address); if(artifact)return new Response(readFileSync(artifact.archive)); const url=new URL(address), path=decodeURIComponent(url.pathname); if(url.origin==="https://registry.npmjs.org") { - for(const [name,item] of Object.entries(fixture.packages)) if(path==="/"+name+${JSON.stringify(`/${version}`)}||path==="/"+name+"/latest")return Response.json(item.manifest); + for(const item of allPackages)if(path==="/"+item.manifest.name+"/"+item.manifest.version)return Response.json(item.manifest); + for(const [name,item] of Object.entries(fixture.packages))if(path==="/"+name+"/latest")return Response.json(item.manifest); + for(const [name,item] of Object.entries(fixture.feature?.packages??{}))if(path==="/"+name+"/atape-managed")return Response.json(item.manifest); } if(url.origin==="https://upgrade-fixture.invalid") { if(path==="/api/v1/instance")return Response.json({protocol:"atape.instance.v1",instance_origin:url.origin,web_origin:url.origin,api_origin:url.origin, @@ -174,7 +211,8 @@ else if(args[0]==="install") { const fixture=JSON.parse(fs.readFileSync(${JSON.stringify(publicRelease)},"utf8")); const digest="sha512-"+crypto.createHash("sha512").update(fs.readFileSync(archive)).digest("base64"); const match=fixture.bundle.packages.find(item=>item.integrity===digest); - if(!match&&digest!==${JSON.stringify(codexClockBaselineIntegrities["@atape/adapter-codex"])})throw Error("Unverified npm archive"); + const featureMatch=fixture.feature?.bundle.packages.find(item=>item.integrity===digest); + if(!match&&!featureMatch&&digest!==${JSON.stringify(codexClockBaselineIntegrities["@atape/adapter-codex"])})throw Error("Unverified npm archive"); if(args.includes("--global")&&(destination!==${JSON.stringify(prefix)}||match?.name!=="@atape/cli"))throw Error("Invalid global install"); cp.execFileSync(${JSON.stringify(realNpm)},["install",...(args.includes("--global")?["--global"]:[]),"--prefix",destination,archive,"--offline","--ignore-scripts","--engine-strict","--no-audit","--no-fund"], {env:{...process.env,PATH:process.env.CODEX_CLOCK_ORIGINAL_PATH,NODE_OPTIONS:""},stdio:"pipe",timeout:120000}); @@ -206,7 +244,9 @@ else if(args[0]==="install") { assert.equal((await json(join(dirname(dirname(bootstrap)),"package.json"))).version,"0.5.9") const helper = join(root,"caller.mjs") await build({entryPoints:[fileURLToPath(new URL("../src/runtime/fixtures/codex-clock-bootstrap-transition.ts",import.meta.url))],outfile:helper,bundle:true, - platform:"node",format:"esm",target:"node24",define:{__ATAPE_CLI_VERSION__:JSON.stringify(version),__ATAPE_CAPTURE_STATE_CONTRACT__:JSON.stringify("atape.client.v3-capture.v2"),"process.env.NODE_ENV":'"production"'}, + // This helper only inspects or starts the selected installed executable. Its + // identity remains capable of reading the final feature's execution floor. + platform:"node",format:"esm",target:"node24",define:{__ATAPE_CLI_VERSION__:JSON.stringify(feature?.version ?? version),__ATAPE_CAPTURE_STATE_CONTRACT__:JSON.stringify("atape.client.v3-capture.v2"),"process.env.NODE_ENV":'"production"'}, banner:{js:'import { createRequire as __atapeCreateRequire } from "node:module"; const require = __atapeCreateRequire(import.meta.url);'},logLevel:"silent"}) const requestFile = join(root,"caller-request.json") const invoke = async phase => { @@ -215,7 +255,7 @@ else if(args[0]==="install") { return JSON.parse(result.stdout) } const terminal = fileURLToPath(new URL("verify-codex-clock-upgrade-terminal.py",import.meta.url)) - const pty = phase => execute("python3",[terminal,bootstrap,root,phase,version],{cwd:root,env:environment,encoding:"utf8",timeout:180_000,maxBuffer:2*1024*1024}) + const pty = (phase, expectedVersion = version) => execute("python3",[terminal,bootstrap,root,phase,expectedVersion],{cwd:root,env:environment,encoding:"utf8",timeout:180_000,maxBuffer:2*1024*1024}) const entries = async()=> (await optionalBytes(trace))?.toString("utf8").split("\n").filter(Boolean).map(JSON.parse) ?? [] const captured = async file => (await optionalBytes(file))?.toString("utf8").split("\n").filter(Boolean).map(JSON.parse) ?? [] const protectedFiles = [join(home,"config","client.json"),join(home,"state","collector.json"),join(home,"state","collector-process.json.desired.json"), @@ -239,7 +279,7 @@ else if(args[0]==="install") { assert.ok((await captured(captures)).every(batch=>batch.canonicalProfileVersion==="atape.acp-centered.v2")) // Match the deployed release/minimum metadata for every upgrade action; // lower historical metadata applies only while seeding the real old capture. - await writeFile(publicRelease,JSON.stringify({ ...distribution,serverVersion:version,minimumCLIVersion:version })) + await writeFile(publicRelease,JSON.stringify({ ...distribution,serverVersion:feature?.version ?? version,minimumCLIVersion:feature?.version ?? version })) await invoke("enable-auto") const beforeAuto=await snapshot(); beforeAuto.set(join(home,"state","collector-process.json"),await readFile(join(home,"state","collector-process.json"))) await invoke("dispatch") @@ -310,12 +350,153 @@ else if(args[0]==="install") { const retained = join(root,"retained-codex") await npm(["install","--prefix",retained,baselineCodex,"--offline","--ignore-scripts","--no-audit","--no-fund"]) const oldCheck=join(root,"old-adapter-caller.mjs") - const oldRequest={protocolVersion:"atape.adapter.v1alpha1",cursor:continued.state.checkpoint.cursor, - rawProgress:continued.state.checkpoint.rawObjects,limits:AdapterCollectionLimits,rawCaptureEnabled:true} - await writeFile(oldCheck,`import assert from "node:assert/strict";\nimport { createAtapeAdapter } from ${JSON.stringify(pathToFileURL(join(retained,"node_modules","@atape","adapter-codex","dist","index.js")).href)};\nconst runtime=await createAtapeAdapter({protocolVersion:"atape.adapter.v1alpha1",adapter:{id:"codex",version:"0.5.9"},project:{id:"upgrade-project",type:"directory",path:${JSON.stringify(join(root,"workspace"))}},signal:AbortSignal.timeout(5000)});\ntry{await assert.rejects(runtime.collect({...${JSON.stringify(oldRequest)},signal:AbortSignal.timeout(5000)}),error=>error.reason==="cursor"&&/cursor is invalid/i.test(error.message));process.stdout.write("old-adapter-refused-v${candidateProjection}");}finally{await runtime.close?.()}\n`) - assert.equal((await execute(process.execPath,[oldCheck],{cwd:root,env:environment,encoding:"utf8",timeout:15_000,maxBuffer:1024*1024})).stdout,`old-adapter-refused-v${candidateProjection}`) + const assertOldAdapterRefuses = async (checkpoint, projection) => { + const oldRequest={protocolVersion:"atape.adapter.v1alpha1",cursor:checkpoint.cursor, + rawProgress:checkpoint.rawObjects,limits:AdapterCollectionLimits,rawCaptureEnabled:true} + await writeFile(oldCheck,`import assert from "node:assert/strict";\nimport { createAtapeAdapter } from ${JSON.stringify(pathToFileURL(join(retained,"node_modules","@atape","adapter-codex","dist","index.js")).href)};\nconst runtime=await createAtapeAdapter({protocolVersion:"atape.adapter.v1alpha1",adapter:{id:"codex",version:"0.5.9"},project:{id:"upgrade-project",type:"directory",path:${JSON.stringify(join(root,"workspace"))}},signal:AbortSignal.timeout(5000)});\ntry{await assert.rejects(runtime.collect({...${JSON.stringify(oldRequest)},signal:AbortSignal.timeout(5000)}),error=>error.reason==="cursor"&&/cursor is invalid/i.test(error.message));process.stdout.write("old-adapter-refused-v${projection}");}finally{await runtime.close?.()}\n`) + assert.equal((await execute(process.execPath,[oldCheck],{cwd:root,env:environment,encoding:"utf8",timeout:15_000,maxBuffer:1024*1024})).stdout,`old-adapter-refused-v${projection}`) + assert.equal(JSON.parse(Buffer.from(checkpoint.cursor,"base64url").toString("utf8")).eventProjectionVersion,projection) + } + await assertOldAdapterRefuses(continued.state.checkpoint,candidateProjection) await unchanged(credentialBefore); assert.equal(await optionalBytes(native),undefined) - const record={baselineVersion:"0.5.9",baselineIntegrities:codexClockBaselineIntegrities,baselineEntrySha256:entryHash, + let featureEvidence + if (feature) { + const bridgeBootstrapBytes = await readFile(bootstrap) + assert.equal(sha256(bridgeBootstrapBytes),candidateEntryHash,"Second leg must retain the genuine installed bridge bootstrap") + const bridgeManifestBytes = await readFile(join(dirname(dirname(bootstrap)),"package.json")) + assert.equal(JSON.parse(bridgeManifestBytes).version,version) + const beforeFeature = await invoke("snapshot"), preceding = await captured(captures), precedingEvents = preceding.flatMap(batch=>batch.events) + const precedingRaw = await readFile(rawCaptures), beforeFeatureNpmCalls = (await captured(calls)).length + const beforeFeatureEntryCount = (await entries()).length + const assertNoRegisteredCaptureJournal = async () => { + assert.equal(await optionalBytes(join(home,"state","collector.json.capture-installation.json")),undefined) + assert.equal(await readdir(join(home,"state","collector.json.captures")).catch(cause=>{if(missing(cause))return undefined;throw cause}),undefined) + } + // Codex uses the Legacy Canonical runtime even for profile v5. It never + // registers a SourceCapture account; the real coordinator completes an + // empty journal inventory while the Adapter separately replays projection. + await assertNoRegisteredCaptureJournal() + // The published bridge's manual UI refreshes the global command even for + // a target-owned bundle. Exercise its genuine automatic worker instead. + // Advance only the external clock Seam so the ordinary durable retry and + // discovery cache expire; do not edit the updater's private state. + distribution.featureAdvertised = true + distribution.clockOffsetMs += 2 * 24 * 60 * 60 * 1000 + await writeFile(publicRelease,JSON.stringify({ ...distribution,serverVersion:feature.version,minimumCLIVersion:feature.version })) + const enabled = await invoke("enable-auto") + assert.equal(enabled.config.autoUpdateEnabled,true) + assert.equal(enabled.wanted,false);assert.equal(enabled.status.running,false) + await invoke("dispatch") + await waitFor(async()=>{ + const state=await json(join(home,"updates","state.json")).catch(cause=>{if(missing(cause))return undefined;throw cause}) + const workers=(await entries()).slice(beforeFeatureEntryCount).filter(entry=>entry.args[0]==="__automatic-update") + const settled=workers.length>0&&workers.every(worker=>!alive(worker.pid)) + if(settled&&state?.failures>0)throw new Error(`Genuine installed bridge automatic update failed: ${state.failure}`) + const selection=await json(join(home,"updates","runtime.json")) + return settled&&state?.version===feature.version&&state.failures===0&&selection.version===feature.version&& + await optionalBytes(join(home,"updates","target-update.json"))===undefined + },"Genuine installed bridge automatic worker did not settle at the target-owned feature") + const selected = await invoke("snapshot"), targetControl = await json(join(home,"updates","control.json")) + const bridgeWorkers = (await entries()).slice(beforeFeatureEntryCount).filter(entry=>entry.args[0]==="__automatic-update") + assert.ok(bridgeWorkers.length>0&&bridgeWorkers.every(entry=>entry.sha256===candidateEntryHash), + "Feature update must execute the exact installed bridge automatic worker") + assert.equal(selected.selection.version,feature.version);assert.equal(selected.effective.adapters[0].version,feature.version) + assert.equal(selected.wanted,false);assert.equal(selected.status.running,false) + assert.equal(selected.config.autoUpdateEnabled,true);assert.equal(selected.config.autoStartEnabled,false) + assert.deepEqual(selected.config.projects,beforeFeature.config.projects);assert.deepEqual(selected.config.enabledAdapterIds,beforeFeature.config.enabledAdapterIds) + assert.deepEqual(selected.state.checkpoint.rawObjects,beforeFeature.state.checkpoint.rawObjects) + assert.equal((await captured(captures)).length,preceding.length,"Stopped target-owned selection must not capture native history") + assert.deepEqual(await readFile(rawCaptures),precedingRaw) + assert.equal(targetControl.phase,"completed");assert.equal(targetControl.floor.minimumRuntimeVersion,feature.version) + assert.equal(targetControl.target.version,feature.version);assert.equal(targetControl.forwardOnly,true) + assert.equal(await optionalBytes(join(home,"updates","target-update.json")),undefined) + assert.equal(await optionalBytes(join(home,"updates","pending.json")),undefined) + assert.equal(await optionalBytes(join(home,"state","collector-process.json.maintenance.json")),undefined) + assert.deepEqual(await readdir(join(home,"updates","target-capture-intents")).catch(cause=>{if(missing(cause))return [];throw cause}),[]) + assert.deepEqual(await readFile(bootstrap),bridgeBootstrapBytes,"Target-owned update must preserve the installed bridge bootstrap") + assert.deepEqual(await readFile(join(dirname(dirname(bootstrap)),"package.json")),bridgeManifestBytes) + const featureNpmCalls = (await captured(calls)).slice(beforeFeatureNpmCalls) + const featureGlobalInstalls = featureNpmCalls.filter(args=>args[0]==="install"&&args.includes("--global")) + assert.deepEqual(featureGlobalInstalls,[],`Feature update may not rewrite the global npm installation: ${JSON.stringify(featureGlobalInstalls)}`) + const featureEntry = join(home,"releases",feature.version,"node_modules","@atape","cli","dist","atape.js") + const featureEntryHash = sha256(await readFile(featureEntry)), packedFeature = (await execute("tar",["-xOf",feature.packages["@atape/cli"].archive,"package/dist/atape.js"],{encoding:"buffer",maxBuffer:16*1024*1024})).stdout + assert.equal(featureEntryHash,sha256(packedFeature)) + const migration = await json(join(home,"updates","capture-migration.json")), requirement = migration.required.requirement, receipt = migration.receipt + assert.equal(migration.protocol,"atape.capture-migration.v1");assert.equal(migration.phase,"completed") + assert.equal(migration.attempt,undefined) + assert.deepEqual(migration.required,await json(join(home,"updates","capture-migration.required.json"))) + assert.equal(requirement.outerKey,targetControl.key);assert.equal(requirement.source.version,version) + assert.deepEqual(requirement.target,targetControl.target) + assert.deepEqual(requirement.bundle.packages,feature.bundle.packages) + assert.deepEqual(requirement.inventory,[],"Legacy Canonical Codex must have no registered SourceCapture journal inventory") + assert.equal(receipt.protocol,"atape.capture-migration-receipt.v1");assert.equal(receipt.planId,"journal-v7-to-v8") + assert.equal(receipt.outerKey,targetControl.key);assert.equal(receipt.requirementHash,requirement.hash) + assert.equal(receipt.bundleFingerprint,requirement.bundleFingerprint) + assert.deepEqual(receipt.target,{version:feature.version,captureStateContract:feature.bundle.captureStateContract}) + assert.deepEqual(receipt.accounts,[]);assert.deepEqual(migration.progress,[]) + await assertNoRegisteredCaptureJournal() + const targetOwner = await json(join(home,"updates","target-owner.json")) + assert.equal(targetOwner.protocol,"atape.update-target.v1");assert.deepEqual(targetOwner.bundle,feature.bundle) + assert.equal(targetOwner.entrySha256,featureEntryHash);assert.equal(targetOwner.bootstrapEntry,bootstrap) + assert.equal(targetOwner.paths.atapeHome,home) + const targetEntries = (await entries()).filter(entry=>entry.args[0]==="__update-target") + assert.ok(targetEntries.some(entry=>entry.args.at(-1)==="prepare")&&targetEntries.some(entry=>entry.args.at(-1)==="activate")) + assert.ok(targetEntries.every(entry=>entry.sha256===featureEntryHash),"Preparation and activation must execute the exact installed target") + await collect() + const replayed = (await captured(captures)).slice(preceding.length), events = replayed.flatMap(batch=>batch.events), featureReplay = await invoke("snapshot") + assert.ok(replayed.length>0&&replayed.every(batch=>batch.canonicalProfileVersion==="atape.acp-centered.v5")) + assert.deepEqual(new Set(events.map(event=>event.sourceEventId)),new Set(precedingEvents.map(event=>event.sourceEventId))) + for(const event of events){assert.equal(event.projectionRevision,7);assert.ok(event.revision>Math.max(...precedingEvents.filter(old=>old.sourceEventId===event.sourceEventId).map(old=>old.revision)))} + assert.equal(events.find(event=>event.sourceEventId==="clock-unknown").occurredAt,null) + const files = events.filter(event=>event.toolUpdateJson).map(event=>JSON.parse(event.toolUpdateJson)).find(update=>update.toolCallId==="clock-files") + assert.deepEqual(files._meta["atape.dev/file-activity"],[{operation:"read",path:"src/main.ts"},{operation:"search",path:"src"}]) + assert.deepEqual(files.rawInput,{parsed_cmd:[{type:"read",path:"src/main.ts"},{type:"search",path:"src"}]}) + assert.equal(replayed[0].session.updatedAt,"2026-10-11T00:00:03Z") + assert.deepEqual(featureReplay.state.checkpoint.rawObjects,beforeFeature.state.checkpoint.rawObjects) + assert.deepEqual(await readFile(rawCaptures),precedingRaw) + assert.equal(JSON.parse(Buffer.from(featureReplay.state.checkpoint.cursor,"base64url").toString("utf8")).eventProjectionVersion,7) + const featureNoOpCount = (await captured(captures)).length + await collect();assert.equal((await captured(captures)).length,featureNoOpCount) + await appendFile(source,lines([item("clock-feature-appended","AgentMessage","A feature continuation","2026-10-11T00:00:04Z")])) + await collect() + const featureContinued = await invoke("snapshot"), featureAppend = (await captured(captures)).slice(featureNoOpCount).flatMap(batch=>batch.events) + assert.deepEqual(featureAppend.map(event=>event.sourceEventId),["clock-feature-appended"]) + assert.ok(featureContinued.state.checkpoint.rawObjects[0].serverOffset>beforeFeature.state.checkpoint.rawObjects[0].serverOffset) + assert.equal(featureContinued.state.checkpoint.rawObjects[0].sourceGeneration,beforeFeature.state.checkpoint.rawObjects[0].sourceGeneration) + assert.equal(featureContinued.wanted,false);assert.equal(featureContinued.status.running,false) + assert.equal(featureContinued.config.autoUpdateEnabled,true) + const featureDaemons = (await entries()).filter(entry=>entry.args[0]==="__collector-daemon"&&entry.sha256===featureEntryHash) + assert.equal(featureDaemons.length,3,"Replay, no-op and append must execute the actual installed feature") + await assertOldAdapterRefuses(featureContinued.state.checkpoint,7) + const disabled = await invoke("disable-auto") + assert.equal(disabled.config.autoUpdateEnabled,false) + assert.equal(disabled.wanted,false);assert.equal(disabled.status.running,false) + assert.equal(disabled.selection.version,feature.version) + assert.deepEqual(disabled.state.checkpoint,featureContinued.state.checkpoint) + await assertNoRegisteredCaptureJournal() + await unchanged(credentialBefore) + assert.deepEqual(await readFile(bootstrap),bridgeBootstrapBytes) + const requested = await captured(requests) + assert.ok(requested.some(request=>request.address===releaseCatalogURL("atape.update-catalog.v3")&&bridgeWorkers.some(worker=>worker.pid===request.pid))) + assert.ok(requested.every(request=>![releaseBundleURL(feature.version,"atape.release-bundle.v1"), + releaseBundleURL(feature.version,"atape.release-bundle.v2")].includes(request.address)),"Target-owned feature discovery must not request a legacy descriptor") + const nativeProbes = await captured(native) + assert.ok(nativeProbes.every(call=>call.rejected===true&&call.method==="execFile"&&Array.isArray(call.args)&& + (basename(call.file)==="launchctl"&&call.args.length===2&&call.args[0]==="print"&&/^gui\/\d+$/.test(call.args[1])|| + basename(call.file)==="systemctl"&&JSON.stringify(call.args)===JSON.stringify(["--user","show-environment"]))),"Native OS registration remains forbidden") + featureEvidence = {version:feature.version,entrySha256:featureEntryHash,cliIntegrity:feature.packages["@atape/cli"].manifest.dist.integrity, + codexIntegrity:feature.packages["@atape/adapter-codex"].manifest.dist.integrity,bundle:feature.bundle, + actualInstalledBridgeAutomaticWorker:true,actualTargetPrepareAndActivate:true,captureMigrationReceiptVerified:true, + emptyInventoryLegacyCodex:true,noNonEmptyJournalMigrationClaim:true,targetOwnerVerified:true,featureLegacyDescriptorsAdvertised:false, + npmLatestRetainedBridge:true,globalBridgeBootstrapBytesPreserved:true,globalNpmInstalls:featureGlobalInstalls.length, + globalNpmReadQueries:featureNpmCalls.filter(args=>["root","prefix"].includes(args[0])&&args.includes("--global")).length, + automaticUpdatesExplicitlyEnabled:true, + automaticUpdatesEnabledThroughReplay:true,automaticUpdatesExplicitlyDisabledAfterReplay:true,automaticUpdatesOffAtEnd:true,stopIntentPreserved:true, + explicitNullReplayed:true,eventIDsPreserved:true,revisionsAdvanced:true,rawReceiptsPreservedAcrossReplay:true, + installedNativeFileActivityReplayed:true,noOpContinuationPassed:true,appendedContinuationPassed:true,genuineOldAdapterRejectedProjection7:true, + installedCandidateDaemonCycles:3,readerFloor:targetControl.floor} + } + const bridgeRecord={baselineVersion:"0.5.9",baselineIntegrities:codexClockBaselineIntegrities,baselineEntrySha256:entryHash, candidateVersion:version,candidateEntrySha256:candidateEntryHash,candidateCLIIntegrity:publicPackages["@atape/cli"].manifest.dist.integrity, candidateCodexIntegrity:publicPackages["@atape/adapter-codex"].manifest.dist.integrity,candidateBundlePackages:bundle.packages, genuineOldAutomaticRejectedBeforePause:true,genuineOldUIRejectedBeforePause:true,genuineOldCollectorCapturedProjection4:true, @@ -324,17 +505,21 @@ else if(args[0]==="install") { revisionsAdvanced:true,noOpContinuationPassed:true,appendedContinuationPassed:true,[`genuineOldAdapterRejectedProjection${candidateProjection}`]:true, releaseChannel:channel,candidateCanonicalProfile:candidateProfile,candidateProjectionVersion:candidateProjection, ...(candidateProjection >= 7 ? { installedNativeFileActivityReplayed: true } : {}), - installedCandidateDaemonCycles:3,upgradeServerRelease:version,upgradeServerMinimumCLI:version, + installedCandidateDaemonCycles:3,upgradeServerRelease:feature?.version ?? version,upgradeServerMinimumCLI:feature?.version ?? version, readerFloor:control.floor,retainedRuntimeRollbackAfterNewCursor:"unsupported; recover forward with target runtime", providerCaptureTested:"constructed native-format source through genuine installed readers",realOSRegistrationTested:false, server:"owned HTTP Test Adapter for Canonical v2/v3/v4/v5 and Raw; Publication unavailable; no deployed Instance touched"} + const record = feature ? {baselineVersion:"0.5.9",releaseVersion:feature.version,releaseChannel:"target-owned",clockBridge:bridgeRecord,feature:featureEvidence, + fullChain:"passed-with-genuine-published-bridge-and-controlled-installed-candidate-artifacts",realOSRegistrationTested:false, + server:bridgeRecord.server} : bridgeRecord await writeFile(join(root,"codex-clock-upgrade-acceptance.json"),JSON.stringify(record,null,2)+"\n",{mode:0o600}) return record } finally {await invoke("cleanup")} } if(process.argv[1]===fileURLToPath(import.meta.url)) { - const [releaseDirectory,fixtureDirectory,baselineCLI,baselineCodex,version]=process.argv.slice(2) - if(!version)throw new Error("usage: verify-codex-clock-upgrade.mjs release-dir fixture-dir genuine-cli-0.5.9.tgz genuine-codex-0.5.9.tgz version") - process.stdout.write(JSON.stringify(await verifyCodexClockUpgrade(releaseDirectory,fixtureDirectory,baselineCLI,baselineCodex,version))+"\n") + const [releaseDirectory,fixtureDirectory,baselineCLI,baselineCodex,version,featureDirectory,featureVersion]=process.argv.slice(2) + if(!version||Boolean(featureDirectory)!==Boolean(featureVersion))throw new Error("usage: verify-codex-clock-upgrade.mjs bridge-dir fixture-dir genuine-cli-0.5.9.tgz genuine-codex-0.5.9.tgz bridge-version [feature-dir feature-version]") + process.stdout.write(JSON.stringify(await verifyCodexClockUpgrade(releaseDirectory,fixtureDirectory,baselineCLI,baselineCodex,version, + featureDirectory?{directory:featureDirectory,version:featureVersion}:undefined))+"\n") } diff --git a/apps/cli/scripts/verify-release.mjs b/apps/cli/scripts/verify-release.mjs index 58ab6d4..3d9d0df 100644 --- a/apps/cli/scripts/verify-release.mjs +++ b/apps/cli/scripts/verify-release.mjs @@ -13,7 +13,7 @@ import { startCLIAuthFixture } from "../../../scripts/cli-auth-fixture.mjs" import { loadReleaseContract } from "../../../scripts/release-contract.mjs" import { verifyToolIdentityRelease } from "./tool-identity-release-gate.mjs" import { verifyCodexClockRelease } from "./codex-clock-release-gate.mjs" -import { verifyActiveBridgeRelease } from "./active-bridge-release-gate.mjs" +import { acquireActiveBridgeBaselines, acquireRetainedActiveBridgeRelease, verifyActiveBridgeRelease } from "./active-bridge-release-gate.mjs" const execute = promisify(execFile) const repositoryRoot = fileURLToPath(new URL("../../..", import.meta.url)) @@ -80,11 +80,28 @@ try { await run(process.execPath, ["--test", fileURLToPath(new URL("tool-identity-release-gate.node.mjs", import.meta.url))], repositoryRoot) await verifyToolIdentityRelease({ version: release.version, releaseDirectory }) await run(process.execPath, ["--test", fileURLToPath(new URL("codex-clock-release-gate.node.mjs", import.meta.url))], repositoryRoot) - const clockTransition = await verifyCodexClockRelease({ version: release.version, releaseDirectory }) + const retainedBridgeRelease = release.releaseChannel === "target-owned" && process.platform !== "win32" + ? await acquireRetainedActiveBridgeRelease(join(temporaryRoot, "retained-bridge")) : undefined + const clockTransition = await verifyCodexClockRelease({ version: release.version, releaseChannel: release.releaseChannel, releaseDirectory, + ...(retainedBridgeRelease ? { bridgeRelease: retainedBridgeRelease } : {}) }) process.stdout.write(`Codex clock release gate: ${JSON.stringify(clockTransition)}\n`) await run(process.execPath, ["--test", fileURLToPath(new URL("active-bridge-release-gate.node.mjs", import.meta.url))], repositoryRoot) - const activeBridge = await verifyActiveBridgeRelease({ version: release.version, releaseChannel: release.releaseChannel, releaseDirectory }) - process.stdout.write(`Active bridge release gate: ${JSON.stringify(activeBridge)}\n`) + if (release.releaseChannel === "target-owned" && process.platform !== "win32") { + // Download each genuine historical archive once; every installed case still + // revalidates its immutable bytes and owns an independent installation. + const bridgeRelease = retainedBridgeRelease + const baselineArchives = await acquireActiveBridgeBaselines(join(temporaryRoot, "active-bridge-baseline")) + for (const faultPoint of [undefined, "control-synced", "first-private-ledger", "before-readiness", "orphan-apply"]) { + const startedAt = Date.now() + const activeBridge = await verifyActiveBridgeRelease({ version: release.version, releaseChannel: release.releaseChannel, + releaseDirectory, bridgeRelease, baselineArchives, ...(faultPoint ? { faultPoint } : {}) }) + assert.equal(activeBridge.required, true, "Target-owned installed acceptance cannot skip a required case") + process.stdout.write(`Active bridge release gate (${faultPoint ?? "full-chain"}, ${Date.now() - startedAt}ms): ${JSON.stringify(activeBridge)}\n`) + } + } else { + const activeBridge = await verifyActiveBridgeRelease({ version: release.version, releaseChannel: release.releaseChannel, releaseDirectory }) + process.stdout.write(`Active bridge release gate: ${JSON.stringify(activeBridge)}\n`) + } await verifyChecksums() await Promise.all([ mkdir(projectDirectory, { recursive: true }), diff --git a/apps/web/package.json b/apps/web/package.json index 36524f6..f13de44 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@atape/web", - "version": "0.5.11", + "version": "0.5.12", "private": true, "type": "module", "scripts": { diff --git a/compose.yaml b/compose.yaml index 9845117..dda28cd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,7 +1,7 @@ name: atape x-atape-server-image: &atape-server-image - image: ${ATAPE_SERVER_IMAGE:-atape-server:0.5.11} + image: ${ATAPE_SERVER_IMAGE:-atape-server:0.5.12} services: database: @@ -72,9 +72,9 @@ services: context: . dockerfile: deploy/server.Dockerfile args: - ATAPE_RELEASE_VERSION: 0.5.11 + ATAPE_RELEASE_VERSION: 0.5.12 ATAPE_RELEASE_EPOCH: auth-v1 - ATAPE_MINIMUM_CLI_VERSION: 0.5.11 + ATAPE_MINIMUM_CLI_VERSION: 0.5.12 restart: unless-stopped init: true security_opt: @@ -115,9 +115,9 @@ services: context: . dockerfile: deploy/web.Dockerfile args: - ATAPE_RELEASE_VERSION: 0.5.11 + ATAPE_RELEASE_VERSION: 0.5.12 ATAPE_RELEASE_EPOCH: auth-v1 - ATAPE_MINIMUM_CLI_VERSION: 0.5.11 + ATAPE_MINIMUM_CLI_VERSION: 0.5.12 VITE_ATAPE_API_ORIGIN: ${ATAPE_API_PUBLIC_URL:-} restart: unless-stopped ports: diff --git a/deploy/server.Dockerfile b/deploy/server.Dockerfile index 80a4125..7e7fdcf 100644 --- a/deploy/server.Dockerfile +++ b/deploy/server.Dockerfile @@ -1,6 +1,6 @@ -ARG ATAPE_RELEASE_VERSION=0.5.11 +ARG ATAPE_RELEASE_VERSION=0.5.12 ARG ATAPE_RELEASE_EPOCH=auth-v1 -ARG ATAPE_MINIMUM_CLI_VERSION=0.5.11 +ARG ATAPE_MINIMUM_CLI_VERSION=0.5.12 FROM golang:1.26.9-alpine AS build diff --git a/deploy/web.Dockerfile b/deploy/web.Dockerfile index 24b9820..ab591b0 100644 --- a/deploy/web.Dockerfile +++ b/deploy/web.Dockerfile @@ -1,6 +1,6 @@ -ARG ATAPE_RELEASE_VERSION=0.5.11 +ARG ATAPE_RELEASE_VERSION=0.5.12 ARG ATAPE_RELEASE_EPOCH=auth-v1 -ARG ATAPE_MINIMUM_CLI_VERSION=0.5.11 +ARG ATAPE_MINIMUM_CLI_VERSION=0.5.12 FROM node:24-alpine AS build diff --git a/docs/adapters/claude.md b/docs/adapters/claude.md index 4af8559..421bcfc 100644 --- a/docs/adapters/claude.md +++ b/docs/adapters/claude.md @@ -1,13 +1,13 @@ # Claude Code Adapter +The 0.5.12 `target-owned` implementation restores Canonical/publication v5 native +tool identity and recorded file activity. The 0.5.11 `legacy-bridge` retains +genuine v4/active-path.2 provider behavior for older clients. Automatic update +preparation runs in the verified target CLI, so the bridge parent does not +parse this v5 Adapter manifest. Legacy discovery retains the bridge. See +[ADR-0126](../architecture/adr/0126-target-owned-managed-updates.md) and +[target-owned updates](../cli/setup-and-adapters.md#target-owned-updates). -The 0.5.11 `legacy-bridge` package retains the published 0.5.10 Adapter -Implementation so existing clients can update automatically. The CLI gains -target-owned update preparation; the official capture profile remains unchanged. -Ordinary native-tool identity and recorded file activity are planned for the -subsequent `target-owned` package, which will replay available history forward. -[ADR-0126](../architecture/adr/0126-target-owned-managed-updates.md) records -the bridge and the target-owned update Interface. Claude captures the source-indicated current conversation through `atape.source-capture.v2`. The Adapter interprets Claude's graph; the shared Host owns attribution, redaction, durable preparation and delivery. The Server selects @@ -22,9 +22,15 @@ direct background launches through the same Interface. Use **Tools and updates** to add Claude to the global tool selection. Installation alone does not enable collection. See the [package README](../../adapters/claude/README.md) and [CLI setup guide](../cli/setup-and-adapters.md). Preserve the complete CLI state -directory when upgrading. A compatible Host and a Server advertising publication -v2 targets and legacy adoption are required. Older Hosts reject the new manifest. -Merging, package publication and Server deployment are separate actions. +directory when upgrading. Current capture requires a compatible Host, +Server publication v5, v2 targets and legacy adoption. Ordinary collection +checks the declared minimum before opening its account journal, discovering +sources or adopting legacy history. Older Hosts reject the new manifest. +The [0.5.12 release notes](../releases/v0.5.12.md) own publication, deployment +and acceptance evidence. The 0.5.11 bridge +Adapter retains the earlier `.2`/Canonical v4 path; its Host, Server and reader +include v5 support. Published 0.5.10 does not support v5. See +[the coordinated upgrade boundary](../cli/setup-and-adapters.md#stable-native-tool-identity-checkout-increment). ## Sources and supported history @@ -135,17 +141,49 @@ deduplicated per source/reason. ## Bounds and Raw policy -Current source profile `claude.jsonl.active-path.2` selects Canonical v4. Explicit -native `mcp__server__tool` names provide MCP identities; `Skill` tool calls with +Current source profile `claude.jsonl.active-path.4` selects Canonical v5 and +uses projection revision 7. Ordinary native `tool_use.name` supplies a stable +tool identity distinct from its bounded display title. Explicit native +`mcp__server__tool` names provide MCP identities; `Skill` tool calls with a nonempty, trimmed `input.skill` provide Skill names. Names and servers are bounded to 500 UTF-8 bytes; invalid or missing identities remain unclassified. An oversized or otherwise unadmitted Skill input also omits identity: the Host needs the original `skill` field to preserve existing field-redaction rules. The original bounded tool input/output and explicit result status retain their existing meaning. Result updates without identity preserve the call's earlier -fact. Command expansion, Skill mentions and reads of `SKILL.md` are not fabricated +fact; explicit null clears it. The Host applies upload redaction to names, +original admitted input/output and copied display facts before delivery. Hidden +identity is cleared instead of combining secret names under a replacement. +Provider name extraction stays in this Adapter. Command expansion, Skill +mentions and reads of `SKILL.md` are not fabricated as Skill calls. This applies to captured root and child Threads. See -[Session Analytics](../session-analytics.md#recorded-mcp-and-skill-calls). +[Session Analytics](../session-analytics.md#stable-tool-grouping) for the separate +aggregation and evidence Interface. + +### Explicit file activity + +The unmodified, admitted input of `Read` supplies `file_path` as a read path; +`Write`, `Edit` and `MultiEdit` supply it as an edit path. `Grep` and `Glob` +supply `input.path` as a search scope, which is not a list of matching files. +These facts use the optional `atape.dev/file-activity` metadata admitted by the +Canonical v5 contract. Root and child Threads use the same +mapping. Outcomes still describe the call, so a requested edit path does not +prove the file changed successfully. + +Paths remain exactly as recorded: relative and absolute paths are not resolved, +trimmed or normalized. Missing paths, controls, invalid Unicode, paths exceeding +2,048 UTF-8 bytes, unadmitted original inputs and metadata over 64 entries or +4,096 JSON UTF-8 bytes explicitly clear the complete path fact with null. Search patterns, tool result +text, Bash command text and MCP arguments do not establish file activity. +A null fact means explicitly unknown coverage; an absent fact on a result update +preserves the invocation's path. Upload redaction can clear the complete +fact when a path or its copied input field is hidden. No redacted placeholder +becomes a synthetic file path. + +Source profile `.4` isolates the new projection from `.3`; v1 source proof +checkpoints and pending projection-6 legacy pages remain readable. The Host +reprojects available history once while retaining existing Raw acknowledgements. +The published 0.5.10 packages do not implement this file activity contract. The source parser admits complete records up to 16 MiB including LF. Default v2 source-page admission is 32 MiB; explicit caller limits still apply. The complete @@ -179,6 +217,28 @@ See [Raw capture policy](../cli/raw-capture.md). ## Recovery and upgrades +The current `.4` path requires an explicit v5 declaration from both the package +and its Host, plus affirmative Server publication v5 support. The Host advertises +Canonical v2/v3/v4/v5 only to a package declaring target v5; older SourceCapture +packages keep their v2/v3/v4 advertisement. The Adapter checks local v5 support +before opening the selected source. There is no silent downgrade to v4. +See [v5 ingestion](../api/canonical-ingestion.md#v5-stable-native-tool-identity) +and [ADR-0124](../architecture/adr/0124-stable-native-tool-identity.md). + +Available authenticated history can replay into the new projection with stable +Event anchors and higher revisions. Already frozen accepted Canonical bytes, +manifests and Raw acknowledgements are retained; an analytics request does not +rewrite them or recover missing source history. New v5 source recovery metadata +carries literal `atape.acp-centered.v5`, which older Hosts cannot read. Recover +forward with a compatible Server/CLI/Adapter bundle and retained state. + +The earlier `.2`/Canonical v4 path used a v3-compatible source-metadata envelope +plus its independent `canonicalProfileV4` fact. Genuine 0.5.8 recovery and the +stopped 0.5.8-to-0.5.9 transition were verified for that earlier scope without +rewriting frozen bytes. That compatibility does not apply to new v5 metadata. +Keep the [0.5.9 release evidence](../releases/v0.5.9.md#upgrade-and-recovery-boundaries) +separate from the current [coordinated upgrade](../cli/setup-and-adapters.md#stable-native-tool-identity-checkout-increment). + The first release with this v2 path, 0.5.4, requires a stopped manual CLI upgrade from 0.5.3 or older; see [the transition steps](../cli/setup-and-adapters.md#explicit-maintenance-and-external-replacement). @@ -245,7 +305,7 @@ source semantics. ## Verification and remaining work -Current acceptance uses the shipped factory's sourceCapture Interface, the shared +Local acceptance uses the Adapter factory's sourceCapture Interface, the shared collection Interface, installed packages and authenticated HTTP/PostgreSQL. The [candidate record](../releases/evidence/claude-active-path-2026-10-09.md) binds completed local checks and their limits separately from final PR gates. @@ -286,6 +346,7 @@ summary-response records do not establish real provider cost completeness. Cross-file Session adoption, nested child histories, child forks and spill collection remain separate work. The retained source corpus is Claude Code 2.1.263 evidence, not a promise for every version or graph shape. Live-provider -browser staging and upgrades from historical published binaries remain unverified; -source-built previous-main compatibility is checked separately. This increment -does not publish packages, deploy a Server or migrate a production database. +browser staging and cross-release Claude publication recovery into v5 are not +established by this source corpus. Source-built previous-main compatibility is +checked separately. See the [0.5.12 release notes](../releases/v0.5.12.md) for +publication, deployment, database operations and acceptance evidence. diff --git a/docs/adapters/codex.md b/docs/adapters/codex.md index 67f9695..aff7cce 100644 --- a/docs/adapters/codex.md +++ b/docs/adapters/codex.md @@ -1,19 +1,25 @@ # Codex Adapter +The 0.5.12 `target-owned` implementation restores Canonical v5 native tool identity +and recorded file activity with projection/cursor v7. The 0.5.11 +`legacy-bridge` retains genuine Canonical v3/projection v5 provider behavior +for older clients. Automatic update preparation runs in the verified target +CLI, and legacy discovery retains the bridge. See +[ADR-0126](../architecture/adr/0126-target-owned-managed-updates.md) and +[target-owned updates](../cli/setup-and-adapters.md#target-owned-updates). -The 0.5.11 `legacy-bridge` package retains the published 0.5.10 Adapter -Implementation so existing clients can update automatically. The CLI gains -target-owned update preparation; the official capture profile remains unchanged. -Ordinary native-tool identity and recorded file activity are planned for the -subsequent `target-owned` package, which will replay available history forward. -[ADR-0126](../architecture/adr/0126-target-owned-managed-updates.md) records -the bridge and the target-owned update Interface. `@atape/adapter-codex` collects project-scoped local Codex rollout files and projects their completed conversation items into ATape's ACP-centered Adapter protocol. The Adapter is an experimental compatibility layer: Codex documents the CLI product, but does not publish the local rollout JSONL layout as a stable integration API. When Raw capture is enabled, unknown or newly introduced Codex records remain available in Raw while being omitted from Canonical until ATape can map them without inventing semantics. The Web conversation flow renders only Canonical events; it does not add “missing capability” placeholders for omitted provider records. For a local build or release tarball, use Tools and updates → Integration maintenance to install its directory or full archive path, then Choose tools to sync. +The 0.5.12 implementation uses Canonical v5 and a v7 cursor. The +[release notes](../releases/v0.5.12.md) own publication, deployment and acceptance +evidence. The 0.5.11 bridge retains Canonical v3 and its +projection-v5 cursor while adding a v5-capable Host, Server and reader. A +supporting Server and complete CLI/Adapter bundle are required; see +[coordinated upgrades](../cli/setup-and-adapters.md#stable-native-tool-identity-checkout-increment). ## Supported source and limits @@ -91,6 +97,59 @@ Completed Codex items map as follows: Private reasoning content is not promoted into Canonical. When Raw capture is enabled, it remains part of the separately uploaded Raw source, subject to the Collector Host's client-side secret redaction. +## Stable native tool facts + +Current Canonical v5 capture records explicit native `function_call` and +`custom_tool_call` names as ordinary tool identities. The closed native item +types `CommandExecution`, `FileChange` and `ImageView` supply those stable names; +their original command or display title remains separate. Native `McpToolCall` +server/tool fields supply an MCP identity. Extension labels, shell text and +missing or invalid names do not establish an identity. Derived child spawn +markers likewise remain without an inferred native tool name. + +Names must be trimmed, well-formed Unicode and fit 500 UTF-8 bytes. Identity +absence preserves the preceding fact on the same call; explicit null clears it. +Explicit outcomes retain their existing meaning. The Adapter retains admitted +original MCP `server`/`tool` fields so upload field rules can hide their copied +identity and display values. The Host applies existing identity, copied-name and +payload redaction before delivery; grouping rules belong to the Server's +[Session Analytics Module](../session-analytics.md#stable-tool-grouping). +See [ADR-0124](../architecture/adr/0124-stable-native-tool-identity.md). +File activity uses only explicit typed source fields described below. + +## Explicit file activity + +The Canonical v5 contract carries bounded typed path facts in +`atape.dev/file-activity`, separately from tool identity and display title: + +| Native item | Recorded paths | Meaning | +| --- | --- | --- | +| `CommandExecution` | `parsed_cmd[].path` for exact native `read` / `search` types | Read targets or search scopes; one call can carry both. | +| `FileChange` | Keys of `changes` with `add`, `update` or `delete` types, plus explicit `move_path` destinations | Reported edit targets; failed status does not establish a successful change. | +| `ImageView` | `path` | Image read target. | + +Repeated operation/path pairs are counted once per folded call. Search scopes +are not matching files. The Adapter keeps the complete bounded original +`parsed_cmd`, `changes` or `path` input so the Host can honor copied-field +redaction, including object keys. No shell command, old `apply_patch` envelope, +function argument, MCP input or result text is classified into file paths. + +Paths retain their original spelling without resolution or normalization. +Invalid Unicode, controls, empty paths, paths above 2,048 UTF-8 bytes, source +inputs outside the shared 64 KiB admission, or path sets above 64 entries or +4,096 JSON UTF-8 bytes explicitly clear the complete activity fact with null. Facts are never truncated +into partial paths. An empty native collection can explicitly clear an earlier +fact; it does not prove no file was touched. Complete native updates with missing path fields also clear old facts with null; +unrelated updates omit the field and preserve earlier facts. Null remains unknown. +Upload masking of a path or its original input clears the copied activity fact. +Image-view display titles use the generic `View image` so a hidden source path +cannot survive as a copied basename. + +Cursor/projection v7 reads complete v5 and v6 state, preserves source ranks, +revision floors and Raw receipts, and replays available Canonical history once. +Older Adapters cannot read the new v7 cursor; recover forward without resetting +state. Published 0.5.10 has neither this projection nor file activity support. + ## Incremental and Raw behavior The Collector accepts empty pages that advance the cursor with `hasMore: true`, @@ -178,27 +237,39 @@ observations; they never authorize source reads. ## Source clock quality and replay -The current Codex paged runtime explicitly declares legacy Canonical v3. A new -Host must acknowledge that capability; the genuine published 0.5.9 Host refuses -the factory. Collection also requires affirmative Server support for nullable -Canonical ingestion. This is a checkout capability and is not evidence that a -new package version has been published. See +The current Codex paged runtime explicitly declares +`legacyCanonicalProfile: "atape.acp-centered.v5"`. Its factory requires explicit +v5 Host support and defers source opening until collection. Collection requires +affirmative Server Canonical v5 support before reading source bytes or advancing +checkpoint/Raw progress. New CLI discovery explicitly opts into v5; default +Server discovery retains v2/v3/v4 for published clients. The published 0.5.10 +Host/Server do not support this revision. The 0.5.11 bridge adds supporting +Host/Server behavior while retaining the earlier official Codex projection. See [v5 ingestion](../api/canonical-ingestion.md#v5-stable-native-tool-identity) and [the package contract](package-manifest.md#paged-observation-runtime-export). -The first managed update from the genuine published 0.5.9 parent with active -Codex collection and this new Adapter candidate is rejected during read-only +The earlier nullable-clock transition from published 0.5.9 to 0.5.10 declared +legacy Canonical v3. Its first managed update with active Codex collection +was rejected during read-only preflight, before the Collector is paused. The older parent does not bind the new manifest capability into the same scope as the target. This applies even when both runtimes use the unchanged capture-state contract. Stop sync before upgrading the CLI and matching Adapter bundle; retain checkpoints and Raw -acknowledgements. The required POSIX release gate executes the genuine published -0.5.9 console's stopped complete-bundle handoff to the exact 0.5.10-or-later -candidate, then real installed target Collector replay, no-op and append cycles. -It verifies preserved configuration, credentials, checkpoint bytes, Raw receipts -and Stop intent through the caller Interfaces, with Instance minimum CLI 0.5.10 -during upgrade. Distribution and Server HTTP are owned external Test Adapters; -the controlled source records do not establish live Provider acceptance or an -upgrade of a personal installation. Follow the +acknowledgements. Historical POSIX acceptance verified the genuine published +0.5.9 console's stopped complete-bundle clock transition. Reaching the 0.5.12 +v5 feature requires a stopped entry replacement into the compatible 0.5.11 +bridge first, followed by that bridge's target-owned automatic worker. The 0.5.9 decoder +does not acquire an unattended clock transition retroactively. + +Candidate-specific installed acceptance must verify the real two-stage path, +preserved configuration, credentials, checkpoint bytes, Raw receipts and Stop +intent, then replay, no-op and append through the installed target Collector. +The test explicitly enables automatic updates for the bridge-to-feature leg, +verifies that the target preserves this preference, then explicitly disables it; +the historical first leg retains updates-off throughout. +The [release notes](../releases/v0.5.12.md) own completed candidate evidence. +Distribution and Server HTTP are owned external Test Adapters; controlled +source records do not establish live Provider acceptance or an upgrade of a +personal installation. Follow the [stopped Codex upgrade](../cli/setup-and-adapters.md#codex-source-clocks-059-to-0510). Other older-parent boundaries remain governed by the [upgrade guide](../cli/setup-and-adapters.md#upgrade-the-cli-and-adapters). @@ -218,13 +289,18 @@ byte-offset coordinate. Append and discovery reordering preserve coordinates; removed files retain their ranks so later files cannot reuse them. Capacity exhaustion fails explicitly rather than folding distinct records together. -Projection v5 replays supported older Canonical cursors once with stable Event -IDs and a higher revision floor, including old active Canonical/Raw pages and -completed progress. Saved Raw acknowledgements are retained. Later pages freeze +Released projection v5 established source-clock correction and once-only replay +of supported older Canonical cursors. Current projection v7 inherits that clock +behavior and replays available older projections to add explicit tool identities and typed file activity +with stable Event IDs and a higher revision floor, including old active +Canonical/Raw pages and completed progress. Saved Raw acknowledgements are +retained. Later pages freeze Session source recency and title at that revision. Unavailable source history cannot be repaired; previously accepted epochs are not guessed away by the Server. Keep checkpoints and use the updated Adapter for forward recovery; -older Adapters cannot read its projection-v5 cursor. +older Adapters, including published 0.5.10, cannot read the new v7 cursor. The +earlier projection-v5 cursor refusal remains part of the 0.5.9-to-0.5.10 clock +transition evidence; it does not establish v7 rollback compatibility. First projection of a changed snapshot makes an additional bounded record scan to compute source recency after final ownership/format selection. Continuations @@ -234,7 +310,8 @@ Canonical profile. Existing uncertain-delivery and redaction-policy constraints continue to apply. [ADR-0122](../architecture/adr/0122-explicit-legacy-source-clocks.md) records the choice and alternatives. -Projection v5 retains v4's cumulative item repair described in +Current projection v7 retains projection v5's clock correction and v4's +cumulative item repair described in [ADR-0055](../architecture/adr/0055-codex-item-update-revisions.md) and preserves unknown tool outcomes. A missing or unrecognized provider status is omitted; it does not mean success or failure. Explicit provider outcomes and numeric diff --git a/docs/adapters/package-manifest.md b/docs/adapters/package-manifest.md index 8473f01..685f78e 100644 --- a/docs/adapters/package-manifest.md +++ b/docs/adapters/package-manifest.md @@ -104,7 +104,7 @@ verifies manifest/runtime agreement and affirms that Canonical profile on the Server before collecting or mutating progress. Neither an ignored optional field nor a null source clock implicitly selects a profile. The published 0.5.10 Host supports v3, as does the genuine 0.5.11 bridge's official Codex Adapter. The -subsequent v5 provider revision requires a supporting v5 Host. +0.5.12 target-owned Adapter requires a supporting v5 Host. See [ADR-0122](../architecture/adr/0122-explicit-legacy-source-clocks.md) and [the v5 boundary](#bounded-source-capture-capability). @@ -210,14 +210,14 @@ See [v4 ingestion](../api/canonical-ingestion.md#v4-normalized-tool-identity) fo closed shapes, null/absence semantics, bounds and redaction. Canonical v5 adds `{"type":"tool","name":"native-name"}` to that namespace. -The unpublished v5 contract also admits optional `atape.dev/file-activity` +The v5 contract also admits optional `atape.dev/file-activity` with bounded explicit read/edit/search path entries; v4 continues rejecting it. Copied source fields must remain admitted for upload redaction. See [recorded file activity](../session-analytics.md#recorded-file-activity). -V4 still accepts only MCP/Skill identities. The subsequent target-owned feature -release's Claude declares publication target v5 and checks explicit v5 Host -support; Codex declares the paged legacy Canonical v5 profile and checks its -local Host advertisement. The 0.5.11 bridge retains genuine v3/v4 providers. +V4 still accepts only MCP/Skill identities. The 0.5.12 target-owned +Claude Adapter declares publication target v5 and checks explicit v5 Host support; +Codex declares the paged legacy Canonical v5 profile and checks its local Host +advertisement. The 0.5.11 bridge retains genuine v3/v4 providers. Server discovery exposes Canonical v5 only to the new opt-in `ATape-Accept-Canonical-Profile: atape.acp-centered.v5`; the default retains v2/v3/v4 for published clients. Publication v5 likewise requires the explicit @@ -240,11 +240,15 @@ it, so v4's historical compatibility encoding does not establish v5 rollback. This increment is not part of the published/deployed 0.5.10 release. A supporting Server, CLI and Adapters must be updated together before collection starts. -The 0.5.11 bridge candidate adds target-owned update capability while retaining -older official provider output. Its publication must precede the v5 feature -release, and legacy discovery retains that bridge indefinitely. See +The 0.5.11 bridge adds target-owned update capability while retaining older +official provider output. Legacy discovery retains that bridge indefinitely, +so returning older clients reach it before the 0.5.12 feature. V5 provider +preparation is owned by the verified target CLI; the bridge parent does not +parse the target Adapter manifest. See [v5 ingestion](../api/canonical-ingestion.md#v5-stable-native-tool-identity) and [upgrade recovery](../cli/setup-and-adapters.md#stable-native-tool-identity-checkout-increment). +The [0.5.12 release notes](../releases/v0.5.12.md) own publication, deployment +and acceptance evidence. Unlike the paged observation runtime's attribution callback, discovery passes source evidence to the Host, which resolves directory/Git membership before diff --git a/docs/cli/setup-and-adapters.md b/docs/cli/setup-and-adapters.md index f916868..d4d84d6 100644 --- a/docs/cli/setup-and-adapters.md +++ b/docs/cli/setup-and-adapters.md @@ -525,8 +525,9 @@ same target even if automatic updates have since been disabled. Published 0.5.10 cannot directly parse the new v5 official Adapter manifests. The 0.5.11 bridge therefore retains the actual old Codex v3 and Claude v4 -Adapter behavior while adding the new updater. Legacy catalog v1/v2 routes and -npm `latest` retain that bridge. Subsequent feature packages use catalog v3 and +Adapter behavior while adding the new updater. Publication policy retains that +bridge on legacy catalog v1/v2 routes and npm `latest`. Feature packages use +catalog v3 and the managed npm channel. A returning old client can still follow the automatic bridge and then feature path; publication order alone is insufficient. @@ -795,12 +796,13 @@ acceptance, OS registration or an upgrade of a personal installation. ### Stable native tool identity: checkout increment -The Host, Server and reader implement Canonical/publication v5 and Analytics v4 -for explicit native identities and recorded file activity. The 0.5.11 candidate -is a genuine update bridge with older official v3/v4 providers; the subsequent -target-owned feature release restores the complete v5 provider increment. -The published 0.5.10 Server does not advertise v5. Implementation in this -checkout is not evidence of publication or deployment. +The 0.5.12 target-owned implementation includes Canonical/publication v5 and +Analytics v4 for explicit native identities and recorded file activity in the +official Codex/Claude providers. The genuine 0.5.11 update bridge retains older +v3/v4 provider behavior and is assigned the legacy discovery routes. The 0.5.10 +Server does not advertise v5. See [the release record](../releases/v0.5.12.md) +for candidate scope and acceptance; implementation, publication and deployment +remain separate actions. Upgrade the Server before advertising the v5 feature target. Each configured Instance must affirm Canonical v5 for Codex and publication target v5 for Claude, @@ -855,14 +857,26 @@ builds and other package managers receive guidance instead of an inferred target If sync cannot resume, use Resume sync and continue or return with sync stopped. The explicit CLI operation prepares and activates the complete CLI/official Adapter bundle through the same coordinator used by automatic updates, with -manual policy. It then refreshes the global npm command entry from the same -verified CLI archive under the same update ownership. Before tool initialization -there are no active Adapters to align, so only the verified entry is installed. -If entry refresh fails after runtime activation, the error identifies the selected -runtime; reopening or retrying checks the actual command-entry version even when -the managed runtime already equals the target. Tools retains an explicit entry -refresh action in that case. -Before replacing the bootstrap, the internal upgrade preserves the effective +manual policy. For catalog v3, both operations preserve the global npm command +entry. Preparation, target execution, migration, ownership and Collector handoff +remain mandatory, including before tool initialization. Once the managed runtime +equals the latest bundle, an older global entry does not create another update +action. Official Adapter versions still align through the complete bundle. + +Historical catalog v1/v2 operations also refresh the global npm command entry +from the same verified CLI archive under the same update ownership. Before tool +initialization, these legacy operations install only the verified entry. If entry +refresh fails after runtime activation, their error identifies the selected +runtime; reopening or retrying checks the actual command-entry version, and Tools +retains an explicit entry refresh action. + +The already published 0.5.11 manual menu still attempts this global refresh after +selecting a v3 target and can report failure when the npm prefix is not writable. +Its automatic worker preserves the entry and completes the target-owned handoff. +The 0.5.12 manual operation follows that same entry-preserving policy; publishing +it does not change the immutable 0.5.11 menu. + +Before a historical bootstrap replacement, the internal upgrade preserves the effective Adapter installation records in the underlying configuration. Their currently selected versions remain selected even if npm fails or the bootstrap digest changes. Built-in manual and automatic updates share one `ATAPE_HOME` update lock @@ -874,7 +888,7 @@ retry retains its recovery receipt when ownership is busy. Neither lock relies on the continued existence of a PID or a removable lock file. Explicit Adapter maintenance remains separate. -With independent control active, the built-in update verifies the new bootstrap +For a historical global-entry replacement with independent control active, the built-in update verifies the new bootstrap directly, creates its immutable recovery copy and durably binds that identity. It preserves the capture contract and reader floor. If the process dies between npm replacement and binding, a later owned startup detects the changed identity diff --git a/docs/releases/evidence/v0.5.11-delivery.json b/docs/releases/evidence/v0.5.11-delivery.json new file mode 100644 index 0000000..c88cdc3 --- /dev/null +++ b/docs/releases/evidence/v0.5.11-delivery.json @@ -0,0 +1,80 @@ +{ + "protocol": "atape.release-delivery.v1", + "recordedAt": "2026-10-12T19:43:08.438709+00:00", + "releaseVersion": "0.5.11", + "status": "publication_complete", + "source": { + "candidateCommit": "702fc32308c78712ce9bbbf57f98421ce471059b", + "waiverCommit": "f9f4bc1074ed3e40a1bede5f99af23581f393178", + "pullRequest": "https://github.com/SingleMai/ATape/pull/234", + "mergeCommit": "2ac1f1ef35443f6a158d2f87ece1bd27acb63fc1", + "mergedAt": "2026-10-12T18:53:19Z", + "testedTreeMatchesMerge": true, + "tree": "a4b661e1aa2453883b62ad2d2423aabce1bb3d19" + }, + "checks": { + "ci": { + "url": "https://github.com/SingleMai/ATape/actions/runs/38289791305", + "testedCommit": "f9f4bc1074ed3e40a1bede5f99af23581f393178", + "result": "passed", + "completedAt": "2026-10-12T18:52:10Z", + "logSHA256": "352c2f580a2865e639ebe935ddaa44bee7d5663aede93b9088d20231c17eb38d", + "fullPostgreSQLIntegration": "passed", + "authenticationMaintenance": "passed 8.02s", + "installedHTTPProviderCorpus": "passed 983.634s", + "physicalENOSPCRecovery": "passed on native Linux" + }, + "security": { + "url": "https://github.com/SingleMai/ATape/actions/runs/38289791297", + "testedCommit": "f9f4bc1074ed3e40a1bede5f99af23581f393178", + "result": "passed", + "requiredJobs": 3 + } + }, + "publication": { + "status": "completed", + "tag": "v0.5.11", + "url": "https://github.com/SingleMai/ATape/actions/runs/38293987130", + "sourceCommit": "2ac1f1ef35443f6a158d2f87ece1bd27acb63fc1", + "conclusion": "success", + "completedAt": "2026-10-12T19:40:05Z", + "log": { + "filename": "atape-v011-release-completed.log", + "sha256": "1ffb1531198f5a7be423bf1dc2492c8eb10fd9a5b184d7b9187cf93d14cee95a" + }, + "artifactComparison": { + "filename": "atape-v011-ci-artifact-comparison.json", + "sha256": "5f2f94c1bb04f0180ea6a19765d3dab77c8e69cbfa5e6f91f17e6d2e3959505e" + }, + "exactLocalAndCIArchiveFiles": 9, + "npmAndGitHub": { + "filename": "publication.json", + "sha256": "86e5ec7cc97dd624ca1b0e385aadf17f556fac5cb39b2144f46e2f932890a13e" + }, + "staticMetadata": { + "filename": "static-publication.json", + "sha256": "08c83af2f340aefc7ef3caffe3b3a9349cbfde9969d76a017e9de10e8d9450eb" + }, + "staticFilesPubliclyVerified": 15, + "anonymousAcceptance": { + "result": "passed", + "filename": "evidence.json", + "sha256": "fa262c49f4185b98d034085e9685df0c3841a48e9ff6602d94ae28d33ce3f8eb", + "requestCount": 116, + "remoteWrites": 0, + "credentialsSent": false, + "scope": "Eight exact npm versions/tarballs and latest identities, nine GitHub assets, legacy/target catalogs and static historical bytes matched the frozen bridge candidate." + } + }, + "server": { + "status": "not_deployed", + "scope": "This genuine bridge is published for old clients. The separately authorized final .12 Server deployment precedes advertising the feature release; no intermediate .11 Server rollout is claimed." + }, + "limits": [ + "All ten manual staging checks remain pending under the candidate-bound manual-only waiver. Automated CI, integration and security gates passed; no automated gate was waived.", + "The earlier local PostgreSQL failures and their diagnostic limits remain in the original .11 candidate acceptance record; complete fresh native Linux PR and Release integration both passed.", + "Public byte identity and decoded provenance content do not establish independent cryptographic signature verification.", + "POSIX installed acceptance uses controlled native source and external HTTP/OS Test Adapters. Native Windows, live Claude/Cursor, native login/wake and complete manual staging remain unverified.", + "Publication does not prove immediate client uptake; discovery cache and automatic scheduling apply." + ] +} diff --git a/docs/releases/evidence/v0.5.12-staging.json b/docs/releases/evidence/v0.5.12-staging.json new file mode 100644 index 0000000..6a9707f --- /dev/null +++ b/docs/releases/evidence/v0.5.12-staging.json @@ -0,0 +1,66 @@ +{ + "protocol": "atape.auth-staging-attestation.v1", + "releaseVersion": "0.5.12", + "authEpoch": "auth-v1", + "status": "pending", + "testedCommit": null, + "completedAt": null, + "operator": null, + "imageDigests": { + "server": null, + "web": null + }, + "backupRestoreId": null, + "checks": [ + { + "id": "candidate-artifacts", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "official-topology", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "github-provider-staging-smoke", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "official-domain-egress-deny-smoke", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "two-user-two-team", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "waf-tls-callback", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "backup-restore", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "observability-secret-sample", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "rollback", + "status": "pending", + "evidenceUrl": null + }, + { + "id": "release-signoff", + "status": "pending", + "evidenceUrl": null + } + ] +} diff --git a/docs/releases/evidence/v0.5.12-update-acceptance.json b/docs/releases/evidence/v0.5.12-update-acceptance.json new file mode 100644 index 0000000..ce4742d --- /dev/null +++ b/docs/releases/evidence/v0.5.12-update-acceptance.json @@ -0,0 +1,1390 @@ +{ + "protocol": "atape.file-activity-release-acceptance.v1", + "releaseVersion": "0.5.12", + "releaseChannel": "target-owned", + "recordedAt": "2026-10-12T20:41:50.778545+00:00", + "scope": "Local final candidate working tree and exact installed tarballs. This is not npm publication or production deployment evidence. Required exact-head CI and Security remain mandatory.", + "sourceBaseline": "2ac1f1ef35443f6a158d2f87ece1bd27acb63fc1", + "nativeSourceRestoration": { + "sourceCommit": "7061967c97ce45fe90503dbfc98fa254792ff2ce", + "scope": "Eight Codex/Claude source and test files exactly restored from the integrated feature increment. Host and runtime retain the genuine .11 implementation; the .12 CLIUpgrade and ToolUpdates Modules also correct the v3 manual global-entry policy." + }, + "checks": [ + { + "id": "release-gates", + "result": "passed", + "log": "atape-v012-release-gates.log", + "sha256": "fd303c9da5aed924ab7da4e59e836db0ac395a47a7b48f76bde3a17ad640dd0b" + }, + { + "id": "dependencies", + "result": "passed", + "log": "atape-v012-dependencies.log", + "sha256": "0c71734e765f42056c7a7421a00962372457e64f6ec3fec9f02d103f09357e57" + }, + { + "id": "go-race", + "result": "passed", + "log": "atape-v012-go-race.log", + "sha256": "19229e564c24108e964003897c817bac68c092bf5c52104c697b8deb37a59a67" + }, + { + "id": "go-fuzz", + "result": "passed", + "log": "atape-v012-go-fuzz.log", + "sha256": "04c266f63ca4ca820656fe5819eee3768ff954c49c3d7673f308f03e8d6e9a7d" + }, + { + "id": "selfhost-config", + "result": "passed", + "log": "atape-v012-selfhost-config.log", + "sha256": "0144975c9e7b744a74af27e1dcf63ee1a1076b4fe958f91bcf38c28d14a37875" + }, + { + "id": "web-rollout-unit", + "result": "passed", + "log": "atape-v012-web-rollout-unit.log", + "sha256": "d889ce067392ba7d469d09e620167780a8a9b8933473c7dd205050b46f4f9e85" + }, + { + "id": "postgres-integration", + "result": "passed", + "log": "atape-v012-postgres-integration.log", + "sha256": "8449957da03c753b816abb372f52c728d40165cba6c55adc00b17ceb910f30ea" + }, + { + "id": "repository-check", + "result": "passed", + "log": "atape-v012-repository-check.log", + "sha256": "f479034a3cea75c852b9e26c3f5f8127a26ae8ce8be4db2704f4f9dec635c1ac" + }, + { + "id": "production-build", + "result": "passed", + "log": "atape-v012-production-build.log", + "sha256": "2b8ef52a046b4814999de2179a36847d657c96a2096833ab36a2a5dbd82b2ccc" + }, + { + "id": "focused-clock-final", + "result": "passed", + "log": "atape-v012-clock-final-focused-r2.log", + "sha256": "82fba1adb72541425808f5882bea9112a149a04c0daa15154e8228bfc38c3c6d" + }, + { + "id": "focused-active-final", + "result": "passed", + "log": "atape-v012-active-final-focused.log", + "sha256": "adb516ec08de50b383d0c663a3e23de72e44969a0d949fb789d1406120f11a28" + }, + { + "id": "formal-release", + "result": "passed", + "log": "atape-v012-formal-release.log", + "sha256": "215f93b4114a22c3d31b53eba087e5910a8a4eb6454056561fce9822e5efd5be" + }, + { + "id": "final-docs", + "result": "passed", + "log": "atape-v012-final-docs.log", + "sha256": "692ee5f143dfb6a68d57523073d5de3bda8c18c487c8ebb7e33c94663656f3f0" + }, + { + "id": "final-architecture", + "result": "passed", + "log": "atape-v012-final-architecture.log", + "sha256": "750e2d288c715991f65112a5507ce91df22d5942fdd688aac94691760a7634ae" + }, + { + "id": "final-release-policy", + "result": "passed", + "log": "atape-v012-final-release-policy.log", + "sha256": "b69365a67b79f30f315b2b2b6d8d42657fbae9155e42181358ab60a70571d0f9" + }, + { + "command": "pnpm test:self-hosting:restore", + "result": "passed", + "log": "atape-v012-selfhost-restore.log", + "sha256": "181199564c907044c25e87b9c4430c8cb2ec36455062cffccbb61e3abf25d3ed", + "scope": "local working-tree verification; final commit image security and production deployment are separate" + }, + { + "command": "pnpm build:release:images candidate", + "result": "passed", + "log": "atape-v012-release-images.log", + "sha256": "ab5d6b29cfd94a17b94d095e941e36efcbd783418329f94f4e2a5522b7f630b1", + "scope": "local working-tree verification; final commit image security and production deployment are separate" + } + ], + "verificationHistory": [ + { + "command": "pnpm test:release", + "attempt": 1, + "result": "failed", + "exitCode": 1, + "log": "/tmp/atape-v012-formal-release-1-failed.log", + "sha256": "9ed5c058904b9e41afd045ccece1b1ac273aaa159f47ce7ab878d391abce8078", + "location": "verify-codex-clock-upgrade-terminal.py:87, second installed bridge console leg", + "observation": "Complete Tools page showed bridge .11 as latest immediately after first leg. The catalog discovery cache was still fresh; the controlled publication had just exposed .12. Repair is being made through actual Check again UI; no gate waiver.", + "repairOutcome": "Real Check again exposed .12, then revealed the distinct immutable .11 manual global-entry bug in attempt 2. Final fixture uses the genuine .11 automatic worker; cache/retry expiry occurs only through the owned Date.now Test Adapter." + }, + { + "command": "pnpm test:release", + "attempt": 2, + "result": "failed", + "exitCode": 1, + "log": "/tmp/atape-v012-formal-release-2-failed.log", + "sha256": "d7ec6c68ead4d709f1ac3dcf306fbf73c15fcf71c6452e16f07e4a558abecb50", + "location": "verify-codex-clock-upgrade-terminal.py:104, genuine .11 manual console after selecting .12", + "observation": "The installed .11 CLI selected the .12 runtime, then attempted global npm entry replacement and reported failure on the deliberately forbidden global .12 install. This is a real limitation of the already published .11 manual menu. The .12 application Module is being repaired and formal .11 to .12 unattended acceptance uses the actual installed .11 automatic worker; .11 bytes are not modified." + }, + { + "command": "repository-check", + "result": "passed-earlier-source", + "status": "passed", + "log": "/tmp/atape-v012-repository-check-before-manual-fix.log", + "sha256": "15042afe6c82665dda335b5139eba9c0f2647b2e1f415ffee8e7712650226a2a", + "scope": "Earlier working tree before the .12 v3 manual-entry policy fix; retained historical evidence, replaced by fresh final check." + }, + { + "command": "production-build", + "result": "passed-earlier-source", + "status": "passed", + "log": "/tmp/atape-v012-production-build-before-manual-fix.log", + "sha256": "a84200429ab453a1630c6cb15587b45a4706ff7d52c23f4035f21e1242395ada", + "scope": "Earlier working tree before the .12 v3 manual-entry policy fix; retained historical evidence, replaced by fresh final check." + }, + { + "command": "pnpm test:release", + "attempt": 3, + "result": "failed", + "exitCode": 1, + "log": "/tmp/atape-v012-formal-release-3-failed.log", + "sha256": "f1e290366a14252b64ed3f3b589d7d90d556872ca6225499c7d910359054e8b1", + "location": "verify-codex-clock-upgrade.mjs:410", + "observation": "All installed package contracts passed. The clock gate reached successful automatic target selection and pending cleanup, then rejected an npm trace containing --global. Checking whether this is a read-only root/prefix lookup; no failure waiver or success claim.", + "repairOutcome": "The npm Test Adapter permits only read-only root/prefix/config queries and exact isolated installs; its feature global install prohibition was unchanged. The overly broad assertion is now the same mutation-specific install-and-global predicate as the active-bridge gate, with exact retained bootstrap/manifest bytes still required.", + "repairEvidence": "/tmp/atape-v012-clock-npm-query-assertion-check.json", + "repairEvidenceSHA256": "89bd620c0405c0f7a1162efd5d28ef854dbb581aaf59e5e55290724e130426ef" + }, + { + "command": "pnpm test:release", + "attempt": 4, + "result": "failed", + "exitCode": 1, + "log": "/tmp/atape-v012-formal-release-4-failed.log", + "sha256": "b2959c9258515aa9bf96c3f7e0c2aecd875b27b28128e0d88a0a1c8f22017618", + "location": "verify-codex-clock-upgrade.mjs:423", + "observation": "Genuine bridge automatic target selection, pending cleanup, retained entry identity and mutation-specific npm checks passed. The newly added at-least-one-journal-account receipt assertion failed. Source registration semantics and exact retained fixture facts are being investigated before changing coverage; no waiver.", + "repairOutcome": "Both current Codex and the historical Codex package use the LegacyCollector. Persistent actual state and source registration semantics confirm no SourceCapture journal registry or capture directory. The gate now asserts their absence before and after upgrade and exact empty inventory/accounts/progress, while retaining completed transaction/exact target ownership binding. Nonempty SourceCapture coverage belongs to the Claude-enabled active gate and separate installed journal migration contract.", + "repairEvidence": "/tmp/atape-v012-clock-empty-inventory-fix-check.json", + "repairEvidenceSHA256": "0c5c149ea7419c54352c021d5ca58d8e1e51bef3a629bd6833d9993f0fcc01af" + }, + { + "command": "direct verifyCodexClockUpgrade against the exact current .12 archives and genuine public .9/.11", + "result": "failed-focused-diagnostic", + "log": "/tmp/atape-v012-clock-final-focused.log", + "sha256": "e13c19159360b05a9293f0563b6546cde33e002c8ba183905dd08ec66af1184b", + "location": "verify-codex-clock-upgrade.mjs:481", + "observation": "All actual target preparation, completed receipt/owner, empty inventory, replay, no-op, append and Stop/auto preference assertions passed. A final assertion incorrectly required a standalone bundle fetch even though catalog v3 embeds the exact complete bundle. The installed .11 worker made the required v3 catalog request. The assertion is being corrected to verify discovery and forbid feature legacy descriptors without requiring redundant IO.", + "repairOutcome": "Retained worker-bound catalog3 discovery and exact receipt/owner bundle identity; now explicitly forbids feature legacy descriptor requests instead of requiring redundant descriptor IO. Fresh independent clock and active fixtures both pass. Active directly verifies one genuine registered journal, with actual observed SQL8 retained as SQL8; separate installed migration contract verifies real controlled SQL7 to SQL8." + } + ], + "manualTargetOwnedCLI": { + "result": "passed", + "callerTests": 39, + "log": "atape-v012-target-manual-ui-evidence.json", + "sha256": "c9546d94ad61ddfc9e1cf8995e9458bb25d4525f25d5d8cfadd04a3d996773f4", + "scope": "Application caller Interface tests verify v3 manual complete-bundle preparation and activation without global entry replacement, Stop, same-version inspection, Adapter alignment and v1/v2 compatibility. These are not a claim that the immutable .11 manual menu succeeded." + }, + "repositoryCheck": { + "cliPassed": 1151, + "cliSkipped": 2, + "applicationPassed": 490, + "browserScenariosPassed": 100, + "limits": "The Linux systemd parser case is platform-inapplicable on macOS; native macOS wake lifecycle is opt-in and was skipped. Required installed POSIX compatibility cases ran separately." + }, + "postgresIntegration": { + "result": "passed", + "authenticationMaintenanceSeconds": 7.65, + "installedHTTPProviderCorpusSeconds": 655.429, + "scope": "Complete fresh .12 run with original production and test timing budgets, before the later CLIUpgrade/ToolUpdates manual-entry policy repair. Provider and Go source are unchanged by that repair; final exact-head CI still reruns the full integration suite. Historical .11 local failures remain in their original record." + }, + "publishedBridge": { + "version": "0.5.11", + "result": "passed", + "log": "evidence.json", + "sha256": "fa262c49f4185b98d034085e9685df0c3841a48e9ff6602d94ae28d33ce3f8eb", + "pinsEvidence": { + "log": "published-bridge-pins.json", + "sha256": "77bace20b5216745d0baa218a48f41093f515f391befc87919209ff9bb824af6" + }, + "scope": "Anonymous npm/GitHub/static public bytes verified against the frozen .11 archives; the eight retained SHA512 acquisition pins were separately checked from actual public response bodies." + }, + "stoppedHistoricalClockTransition": { + "result": "passed", + "required": true, + "evidence": { + "baselineVersion": "0.5.9", + "releaseVersion": "0.5.12", + "releaseChannel": "target-owned", + "clockBridge": { + "baselineVersion": "0.5.9", + "baselineIntegrities": { + "@atape/cli": "sha512-w47H6TUqDg9bWLLKvYQ8J6jgMz0zT3MLs/wzKy9ZFCkkoM+2iReNUneYEeDM/YNHb3xzKP2pbrD1jdRcCQeehw==", + "@atape/adapter-codex": "sha512-caOzjllMYtbhyM4dU99AodrNrl48X63f3NedW09otADLyCiR5IpkpLMnNp6DOpLqiXvYVgOBs7Zha0O61RGlEw==" + }, + "baselineEntrySha256": "30fa103b1b2dccd98c7e3f29ae1bcdb0e813f3face74e2718af0c41b7972c42a", + "candidateVersion": "0.5.11", + "candidateEntrySha256": "c1fe6589fc5a04a3dbcfca6d1dbe4cdd64c9736551d3aa7a385470a987bae4f7", + "candidateCLIIntegrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "candidateCodexIntegrity": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "candidateBundlePackages": [ + { + "name": "@atape/cli", + "integrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.11.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-fgkgFrNltDa6+Vq/0+SE4xmqvCyPL1SdYgP0KIDu/uas2Oz5OyLly/562zzMkfKfymuFCZUGALqzMMbQ8BLw9Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.11.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-WD05HLDZyZg+24SAm9MAjk0Ob+tMoyuzI5Kk/teESNzWNX5aN7X8sCLAcls7NyaQg4uOmRfWAkclvKAR5CW4YQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-3BbggtxinR210yLzqk1GKaRxbT1aWnnv9Xe6/NWDvzkBh5lWnawva6azCGeF9gKjr7UjZgEUKxihzRgQ9eBqKA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.11.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-y8iAqWqBT+I2E22ofgjVgATIFE9GrMGkCbpSpNw8UwtDx13GyUkijb0YmktcB0sQ7TWr8gmThL9wWeuLlKXacA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.11.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-sKY39aSPOt55XWtHZ6jrLZvcukIMdtfdfjqv+c0p3G1L6EqMZD6ai9mHigy1xcms17Xp/tB5BShPWrvw/PNICw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.11.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-Ggc3vy7laTb2imRSjK6YvbLqKkSbF1Yl7I5v+yI5BxuwzBe4t8DnQg3Yqn9zs2oQi7X9SsxH7HhyrBqy2UAfSA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.11.tgz" + } + ], + "genuineOldAutomaticRejectedBeforePause": true, + "genuineOldUIRejectedBeforePause": true, + "genuineOldCollectorCapturedProjection4": true, + "installedStoppedWholeBundleMenuCompleted": true, + "stopIntentPreserved": true, + "projectsAndToolsPreserved": true, + "credentialBytesPreserved": true, + "checkpointBytesPreservedAtSelection": true, + "rawReceiptsPreservedAcrossReplay": true, + "explicitNullReplayed": true, + "eventIDsPreserved": true, + "revisionsAdvanced": true, + "noOpContinuationPassed": true, + "appendedContinuationPassed": true, + "genuineOldAdapterRejectedProjection5": true, + "releaseChannel": "legacy-bridge", + "candidateCanonicalProfile": "atape.acp-centered.v3", + "candidateProjectionVersion": 5, + "installedCandidateDaemonCycles": 3, + "upgradeServerRelease": "0.5.12", + "upgradeServerMinimumCLI": "0.5.12", + "readerFloor": { + "minimumRuntimeVersion": "0.5.11", + "captureStateContract": "atape.client.v3-capture.v2" + }, + "retainedRuntimeRollbackAfterNewCursor": "unsupported; recover forward with target runtime", + "providerCaptureTested": "constructed native-format source through genuine installed readers", + "realOSRegistrationTested": false, + "server": "owned HTTP Test Adapter for Canonical v2/v3/v4/v5 and Raw; Publication unavailable; no deployed Instance touched" + }, + "feature": { + "version": "0.5.12", + "entrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "cliIntegrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "codexIntegrity": "sha512-s9lJPBMZMMyojIhg0wkGDoICarfMLwrxugdQV/r0qzCz4Zy9lyYVKMiUL939TaB0lij9OX8Zr49DuDDyxVO9WQ==", + "bundle": { + "protocol": "atape.release-bundle.v3", + "version": "0.5.12", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "updateTargetProtocol": "atape.update-target.v1", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ], + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-s9lJPBMZMMyojIhg0wkGDoICarfMLwrxugdQV/r0qzCz4Zy9lyYVKMiUL939TaB0lij9OX8Zr49DuDDyxVO9WQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.12.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-lPviL9eCfRhWhHLmRy2oJOgxAe/tMVQ3Bkz6UcJK8ucStmxAT9Hw3TDdlPs7HI/tX3EbGmm4qGRL5tRMSt2o4Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.12.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-g6ZbiMuzR/4tmuhGfP+w6420+u57K/k6Sap99OPdS1M9wjWhTEQVuQ9f05UnSwqxqazY9xqaQMycWumuNesxRQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-b8woaxKsz9fqok8tqntCiHi3FaKugV7oW99zOSItXLO5uncAoN9vGAy/NWuotJH2kAmvZ30W7DZtKAptiEOREg==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.12.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-sysne93OUKTvMpWkcXaVdyfZoTK6GqHHFwFAb5jjFjXtUQ1sH0qoVJZBZxZjf2ZLIKEuz7GZSTOxqJ1Vg+DG/w==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.12.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-adVnbre65Ed6IdWNk3riiVeIMx9B9WaHjT4tNcpv0Owah5/3zHuWbcN73wNqAf+fX6wMq0bbQY3+4hDKuVhcjA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.12.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-uizSGMwP4DaNTMqZYAarVXNi6FVy+Sjn9vl75+6KLItXLUCdcr2awBRhqlx256SiIOYPcKAs+RSmdsMsqZDPTQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.12.tgz" + } + ] + }, + "actualInstalledBridgeAutomaticWorker": true, + "actualTargetPrepareAndActivate": true, + "captureMigrationReceiptVerified": true, + "emptyInventoryLegacyCodex": true, + "noNonEmptyJournalMigrationClaim": true, + "targetOwnerVerified": true, + "featureLegacyDescriptorsAdvertised": false, + "npmLatestRetainedBridge": true, + "globalBridgeBootstrapBytesPreserved": true, + "globalNpmInstalls": 0, + "globalNpmReadQueries": 6, + "automaticUpdatesExplicitlyEnabled": true, + "automaticUpdatesEnabledThroughReplay": true, + "automaticUpdatesExplicitlyDisabledAfterReplay": true, + "automaticUpdatesOffAtEnd": true, + "stopIntentPreserved": true, + "explicitNullReplayed": true, + "eventIDsPreserved": true, + "revisionsAdvanced": true, + "rawReceiptsPreservedAcrossReplay": true, + "installedNativeFileActivityReplayed": true, + "noOpContinuationPassed": true, + "appendedContinuationPassed": true, + "genuineOldAdapterRejectedProjection7": true, + "installedCandidateDaemonCycles": 3, + "readerFloor": { + "minimumRuntimeVersion": "0.5.12", + "captureStateContract": "atape.client.v3-capture.v2" + } + }, + "fullChain": "passed-with-genuine-published-bridge-and-controlled-installed-candidate-artifacts", + "realOSRegistrationTested": false, + "server": "owned HTTP Test Adapter for Canonical v2/v3/v4/v5 and Raw; Publication unavailable; no deployed Instance touched" + }, + "scope": "Genuine .9 stopped manual transition to the actual .11 bridge, followed by its genuine installed automatic worker to v3-only .12 in one disposable installation. The test explicitly enables auto for the second leg, verifies preservation, then explicitly disables it. Stop intent persists throughout. This does not make the historical .9 clock boundary automatic." + }, + "installedAutomaticChain": { + "baselineVersion": "0.5.10", + "bridgeVersion": "0.5.11", + "featureVersion": "0.5.12", + "cases": [ + { + "case": "full-chain", + "elapsedMilliseconds": 31150, + "required": true, + "result": "passed", + "evidence": { + "baselineVersion": "0.5.10", + "baselineIntegrities": { + "@atape/cli": "sha512-edQJ66nr0VMaKgxYaRDOCiSwJeueEVRTRtBQFk815skIPt7sBOR8ZCfFNhMhOHQ+nU/iY8BEsUxQHoO/LPSCJQ==", + "@atape/adapter-codex": "sha512-tgiD29qMmiH+3DlXtm94XOYBZlVT/N8uXTgX739r9FruGG0Xi4T4M705xbNmhpnrigH9KtzLmyEinEaVHMvfKA==", + "@atape/adapter-claude": "sha512-VDtK5uXgp2vSyhfqKNedpDxFa91R4nfJgrOXH2Hs0O16LhT7RS1dKxiFPhao1q3UReD31xHPnXQcmC8v4oUWYw==" + }, + "baselineEntrySha256": "a1c8ca45d6b338e312b3bb7fe3a40126166d9208278d04a9d3b460537c8f656d", + "bridgeVersion": "0.5.11", + "bridgeEntrySha256": "c1fe6589fc5a04a3dbcfca6d1dbe4cdd64c9736551d3aa7a385470a987bae4f7", + "bridgeCLIIntegrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "bridgeBundle": { + "protocol": "atape.release-bundle.v2", + "version": "0.5.11", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "migration": { + "protocol": "atape.capture-migration.v1", + "id": "journal-v7-to-v8", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ] + }, + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.11.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-fgkgFrNltDa6+Vq/0+SE4xmqvCyPL1SdYgP0KIDu/uas2Oz5OyLly/562zzMkfKfymuFCZUGALqzMMbQ8BLw9Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.11.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-WD05HLDZyZg+24SAm9MAjk0Ob+tMoyuzI5Kk/teESNzWNX5aN7X8sCLAcls7NyaQg4uOmRfWAkclvKAR5CW4YQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-3BbggtxinR210yLzqk1GKaRxbT1aWnnv9Xe6/NWDvzkBh5lWnawva6azCGeF9gKjr7UjZgEUKxihzRgQ9eBqKA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.11.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-y8iAqWqBT+I2E22ofgjVgATIFE9GrMGkCbpSpNw8UwtDx13GyUkijb0YmktcB0sQ7TWr8gmThL9wWeuLlKXacA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.11.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-sKY39aSPOt55XWtHZ6jrLZvcukIMdtfdfjqv+c0p3G1L6EqMZD6ai9mHigy1xcms17Xp/tB5BShPWrvw/PNICw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.11.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-Ggc3vy7laTb2imRSjK6YvbLqKkSbF1Yl7I5v+yI5BxuwzBe4t8DnQg3Yqn9zs2oQi7X9SsxH7HhyrBqy2UAfSA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.11.tgz" + } + ] + }, + "actualPublishedOldAutomaticWorker": true, + "oldManifestDecoderAcceptedActualBridgePackages": true, + "activeCollectorContinued": true, + "wantedPreserved": true, + "bootstrapBytesPreservedAtFirstHandoff": true, + "globalBootstrapBytesPreserved": true, + "globalNPMPrefixReadOnly": true, + "automaticGlobalInstallForbidden": true, + "projectsAndToolsPreserved": true, + "credentialBytesPreserved": true, + "acknowledgedCaptureProgressPreserved": true, + "canonicalBridgeNoReplay": true, + "rawReceiptsPreserved": true, + "appendedContinuation": true, + "stopAndUpdatesOffPreserved": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "actualBaselineClaudeJournalRegistered": true, + "baselineJournalVersion": 8, + "fullChain": "passed-with-controlled-installed-candidate-artifacts", + "feature": { + "version": "0.5.12", + "entrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "cliIntegrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "bundle": { + "protocol": "atape.release-bundle.v3", + "version": "0.5.12", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "updateTargetProtocol": "atape.update-target.v1", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ], + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-s9lJPBMZMMyojIhg0wkGDoICarfMLwrxugdQV/r0qzCz4Zy9lyYVKMiUL939TaB0lij9OX8Zr49DuDDyxVO9WQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.12.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-lPviL9eCfRhWhHLmRy2oJOgxAe/tMVQ3Bkz6UcJK8ucStmxAT9Hw3TDdlPs7HI/tX3EbGmm4qGRL5tRMSt2o4Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.12.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-g6ZbiMuzR/4tmuhGfP+w6420+u57K/k6Sap99OPdS1M9wjWhTEQVuQ9f05UnSwqxqazY9xqaQMycWumuNesxRQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-b8woaxKsz9fqok8tqntCiHi3FaKugV7oW99zOSItXLO5uncAoN9vGAy/NWuotJH2kAmvZ30W7DZtKAptiEOREg==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.12.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-sysne93OUKTvMpWkcXaVdyfZoTK6GqHHFwFAb5jjFjXtUQ1sH0qoVJZBZxZjf2ZLIKEuz7GZSTOxqJ1Vg+DG/w==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.12.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-adVnbre65Ed6IdWNk3riiVeIMx9B9WaHjT4tNcpv0Owah5/3zHuWbcN73wNqAf+fX6wMq0bbQY3+4hDKuVhcjA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.12.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-uizSGMwP4DaNTMqZYAarVXNi6FVy+Sjn9vl75+6KLItXLUCdcr2awBRhqlx256SiIOYPcKAs+RSmdsMsqZDPTQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.12.tgz" + } + ] + }, + "actualBridgeAutomaticWorker": true, + "actualTargetPrepareAndActivate": true, + "nonEmptyCaptureMigrationReceiptVerified": true, + "registeredCaptureAccountsVerified": 1, + "sourceJournalVersion": 8, + "targetJournalVersion": 8, + "targetOwnerVerified": true, + "nativeV5Replay": true, + "eventIDsPreserved": true, + "revisionsAdvanced": true, + "rawReceiptsPreservedAcrossReplay": true, + "nativeFileActivityReplayed": true, + "appendedContinuation": true, + "lateOfflineLegacyRoutesRetained": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "globalBootstrapBytesPreserved": true, + "serverUpgradedBeforeLegacyClient": true, + "serverVersionReadbacks": 6 + }, + "providerCaptureTested": "constructed native-format Codex source through genuine installed readers; actual Claude packages installed and admitted, Claude live source not exercised", + "realOSRegistrationTested": false, + "rejectedReadOnlyManagerProbes": 0, + "server": "owned authenticated HTTP Test Adapter; no deployed Instance touched" + } + }, + { + "case": "control-synced", + "elapsedMilliseconds": 34420, + "required": true, + "result": "passed", + "evidence": { + "baselineVersion": "0.5.10", + "baselineIntegrities": { + "@atape/cli": "sha512-edQJ66nr0VMaKgxYaRDOCiSwJeueEVRTRtBQFk815skIPt7sBOR8ZCfFNhMhOHQ+nU/iY8BEsUxQHoO/LPSCJQ==", + "@atape/adapter-codex": "sha512-tgiD29qMmiH+3DlXtm94XOYBZlVT/N8uXTgX739r9FruGG0Xi4T4M705xbNmhpnrigH9KtzLmyEinEaVHMvfKA==", + "@atape/adapter-claude": "sha512-VDtK5uXgp2vSyhfqKNedpDxFa91R4nfJgrOXH2Hs0O16LhT7RS1dKxiFPhao1q3UReD31xHPnXQcmC8v4oUWYw==" + }, + "baselineEntrySha256": "a1c8ca45d6b338e312b3bb7fe3a40126166d9208278d04a9d3b460537c8f656d", + "bridgeVersion": "0.5.11", + "bridgeEntrySha256": "c1fe6589fc5a04a3dbcfca6d1dbe4cdd64c9736551d3aa7a385470a987bae4f7", + "bridgeCLIIntegrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "bridgeBundle": { + "protocol": "atape.release-bundle.v2", + "version": "0.5.11", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "migration": { + "protocol": "atape.capture-migration.v1", + "id": "journal-v7-to-v8", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ] + }, + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.11.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-fgkgFrNltDa6+Vq/0+SE4xmqvCyPL1SdYgP0KIDu/uas2Oz5OyLly/562zzMkfKfymuFCZUGALqzMMbQ8BLw9Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.11.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-WD05HLDZyZg+24SAm9MAjk0Ob+tMoyuzI5Kk/teESNzWNX5aN7X8sCLAcls7NyaQg4uOmRfWAkclvKAR5CW4YQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-3BbggtxinR210yLzqk1GKaRxbT1aWnnv9Xe6/NWDvzkBh5lWnawva6azCGeF9gKjr7UjZgEUKxihzRgQ9eBqKA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.11.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-y8iAqWqBT+I2E22ofgjVgATIFE9GrMGkCbpSpNw8UwtDx13GyUkijb0YmktcB0sQ7TWr8gmThL9wWeuLlKXacA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.11.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-sKY39aSPOt55XWtHZ6jrLZvcukIMdtfdfjqv+c0p3G1L6EqMZD6ai9mHigy1xcms17Xp/tB5BShPWrvw/PNICw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.11.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-Ggc3vy7laTb2imRSjK6YvbLqKkSbF1Yl7I5v+yI5BxuwzBe4t8DnQg3Yqn9zs2oQi7X9SsxH7HhyrBqy2UAfSA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.11.tgz" + } + ] + }, + "actualPublishedOldAutomaticWorker": true, + "oldManifestDecoderAcceptedActualBridgePackages": true, + "activeCollectorContinued": true, + "wantedPreserved": true, + "bootstrapBytesPreservedAtFirstHandoff": true, + "globalBootstrapBytesPreserved": true, + "globalNPMPrefixReadOnly": true, + "automaticGlobalInstallForbidden": true, + "projectsAndToolsPreserved": true, + "credentialBytesPreserved": true, + "acknowledgedCaptureProgressPreserved": true, + "canonicalBridgeNoReplay": true, + "rawReceiptsPreserved": true, + "appendedContinuation": true, + "stopAndUpdatesOffPreserved": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "actualBaselineClaudeJournalRegistered": true, + "baselineJournalVersion": 8, + "fullChain": "passed-with-controlled-installed-candidate-artifacts", + "feature": { + "version": "0.5.12", + "entrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "cliIntegrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "bundle": { + "protocol": "atape.release-bundle.v3", + "version": "0.5.12", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "updateTargetProtocol": "atape.update-target.v1", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ], + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-s9lJPBMZMMyojIhg0wkGDoICarfMLwrxugdQV/r0qzCz4Zy9lyYVKMiUL939TaB0lij9OX8Zr49DuDDyxVO9WQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.12.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-lPviL9eCfRhWhHLmRy2oJOgxAe/tMVQ3Bkz6UcJK8ucStmxAT9Hw3TDdlPs7HI/tX3EbGmm4qGRL5tRMSt2o4Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.12.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-g6ZbiMuzR/4tmuhGfP+w6420+u57K/k6Sap99OPdS1M9wjWhTEQVuQ9f05UnSwqxqazY9xqaQMycWumuNesxRQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-b8woaxKsz9fqok8tqntCiHi3FaKugV7oW99zOSItXLO5uncAoN9vGAy/NWuotJH2kAmvZ30W7DZtKAptiEOREg==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.12.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-sysne93OUKTvMpWkcXaVdyfZoTK6GqHHFwFAb5jjFjXtUQ1sH0qoVJZBZxZjf2ZLIKEuz7GZSTOxqJ1Vg+DG/w==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.12.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-adVnbre65Ed6IdWNk3riiVeIMx9B9WaHjT4tNcpv0Owah5/3zHuWbcN73wNqAf+fX6wMq0bbQY3+4hDKuVhcjA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.12.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-uizSGMwP4DaNTMqZYAarVXNi6FVy+Sjn9vl75+6KLItXLUCdcr2awBRhqlx256SiIOYPcKAs+RSmdsMsqZDPTQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.12.tgz" + } + ] + }, + "actualBridgeAutomaticWorker": true, + "actualTargetPrepareAndActivate": true, + "nonEmptyCaptureMigrationReceiptVerified": true, + "registeredCaptureAccountsVerified": 1, + "sourceJournalVersion": 8, + "targetJournalVersion": 8, + "targetOwnerVerified": true, + "nativeV5Replay": true, + "eventIDsPreserved": true, + "revisionsAdvanced": true, + "rawReceiptsPreservedAcrossReplay": true, + "nativeFileActivityReplayed": true, + "appendedContinuation": true, + "lateOfflineLegacyRoutesRetained": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "globalBootstrapBytesPreserved": true, + "serverUpgradedBeforeLegacyClient": true, + "serverVersionReadbacks": 6, + "recovery": { + "point": "control-synced", + "actualTargetEntrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "actualParentSIGKILL": true, + "actualOrphanSIGKILL": true, + "actualOldPublicBootstrapRecovery": true, + "completedControlThroughout": true + } + }, + "providerCaptureTested": "constructed native-format Codex source through genuine installed readers; actual Claude packages installed and admitted, Claude live source not exercised", + "realOSRegistrationTested": false, + "rejectedReadOnlyManagerProbes": 1, + "server": "owned authenticated HTTP Test Adapter; no deployed Instance touched" + } + }, + { + "case": "first-private-ledger", + "elapsedMilliseconds": 31464, + "required": true, + "result": "passed", + "evidence": { + "baselineVersion": "0.5.10", + "baselineIntegrities": { + "@atape/cli": "sha512-edQJ66nr0VMaKgxYaRDOCiSwJeueEVRTRtBQFk815skIPt7sBOR8ZCfFNhMhOHQ+nU/iY8BEsUxQHoO/LPSCJQ==", + "@atape/adapter-codex": "sha512-tgiD29qMmiH+3DlXtm94XOYBZlVT/N8uXTgX739r9FruGG0Xi4T4M705xbNmhpnrigH9KtzLmyEinEaVHMvfKA==", + "@atape/adapter-claude": "sha512-VDtK5uXgp2vSyhfqKNedpDxFa91R4nfJgrOXH2Hs0O16LhT7RS1dKxiFPhao1q3UReD31xHPnXQcmC8v4oUWYw==" + }, + "baselineEntrySha256": "a1c8ca45d6b338e312b3bb7fe3a40126166d9208278d04a9d3b460537c8f656d", + "bridgeVersion": "0.5.11", + "bridgeEntrySha256": "c1fe6589fc5a04a3dbcfca6d1dbe4cdd64c9736551d3aa7a385470a987bae4f7", + "bridgeCLIIntegrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "bridgeBundle": { + "protocol": "atape.release-bundle.v2", + "version": "0.5.11", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "migration": { + "protocol": "atape.capture-migration.v1", + "id": "journal-v7-to-v8", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ] + }, + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.11.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-fgkgFrNltDa6+Vq/0+SE4xmqvCyPL1SdYgP0KIDu/uas2Oz5OyLly/562zzMkfKfymuFCZUGALqzMMbQ8BLw9Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.11.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-WD05HLDZyZg+24SAm9MAjk0Ob+tMoyuzI5Kk/teESNzWNX5aN7X8sCLAcls7NyaQg4uOmRfWAkclvKAR5CW4YQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-3BbggtxinR210yLzqk1GKaRxbT1aWnnv9Xe6/NWDvzkBh5lWnawva6azCGeF9gKjr7UjZgEUKxihzRgQ9eBqKA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.11.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-y8iAqWqBT+I2E22ofgjVgATIFE9GrMGkCbpSpNw8UwtDx13GyUkijb0YmktcB0sQ7TWr8gmThL9wWeuLlKXacA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.11.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-sKY39aSPOt55XWtHZ6jrLZvcukIMdtfdfjqv+c0p3G1L6EqMZD6ai9mHigy1xcms17Xp/tB5BShPWrvw/PNICw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.11.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-Ggc3vy7laTb2imRSjK6YvbLqKkSbF1Yl7I5v+yI5BxuwzBe4t8DnQg3Yqn9zs2oQi7X9SsxH7HhyrBqy2UAfSA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.11.tgz" + } + ] + }, + "actualPublishedOldAutomaticWorker": true, + "oldManifestDecoderAcceptedActualBridgePackages": true, + "activeCollectorContinued": true, + "wantedPreserved": true, + "bootstrapBytesPreservedAtFirstHandoff": true, + "globalBootstrapBytesPreserved": true, + "globalNPMPrefixReadOnly": true, + "automaticGlobalInstallForbidden": true, + "projectsAndToolsPreserved": true, + "credentialBytesPreserved": true, + "acknowledgedCaptureProgressPreserved": true, + "canonicalBridgeNoReplay": true, + "rawReceiptsPreserved": true, + "appendedContinuation": true, + "stopAndUpdatesOffPreserved": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "actualBaselineClaudeJournalRegistered": true, + "baselineJournalVersion": 8, + "fullChain": "passed-with-controlled-installed-candidate-artifacts", + "feature": { + "version": "0.5.12", + "entrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "cliIntegrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "bundle": { + "protocol": "atape.release-bundle.v3", + "version": "0.5.12", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "updateTargetProtocol": "atape.update-target.v1", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ], + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-s9lJPBMZMMyojIhg0wkGDoICarfMLwrxugdQV/r0qzCz4Zy9lyYVKMiUL939TaB0lij9OX8Zr49DuDDyxVO9WQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.12.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-lPviL9eCfRhWhHLmRy2oJOgxAe/tMVQ3Bkz6UcJK8ucStmxAT9Hw3TDdlPs7HI/tX3EbGmm4qGRL5tRMSt2o4Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.12.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-g6ZbiMuzR/4tmuhGfP+w6420+u57K/k6Sap99OPdS1M9wjWhTEQVuQ9f05UnSwqxqazY9xqaQMycWumuNesxRQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-b8woaxKsz9fqok8tqntCiHi3FaKugV7oW99zOSItXLO5uncAoN9vGAy/NWuotJH2kAmvZ30W7DZtKAptiEOREg==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.12.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-sysne93OUKTvMpWkcXaVdyfZoTK6GqHHFwFAb5jjFjXtUQ1sH0qoVJZBZxZjf2ZLIKEuz7GZSTOxqJ1Vg+DG/w==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.12.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-adVnbre65Ed6IdWNk3riiVeIMx9B9WaHjT4tNcpv0Owah5/3zHuWbcN73wNqAf+fX6wMq0bbQY3+4hDKuVhcjA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.12.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-uizSGMwP4DaNTMqZYAarVXNi6FVy+Sjn9vl75+6KLItXLUCdcr2awBRhqlx256SiIOYPcKAs+RSmdsMsqZDPTQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.12.tgz" + } + ] + }, + "actualBridgeAutomaticWorker": true, + "actualTargetPrepareAndActivate": true, + "nonEmptyCaptureMigrationReceiptVerified": true, + "registeredCaptureAccountsVerified": 1, + "sourceJournalVersion": 8, + "targetJournalVersion": 8, + "targetOwnerVerified": true, + "nativeV5Replay": true, + "eventIDsPreserved": true, + "revisionsAdvanced": true, + "rawReceiptsPreservedAcrossReplay": true, + "nativeFileActivityReplayed": true, + "appendedContinuation": true, + "lateOfflineLegacyRoutesRetained": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "globalBootstrapBytesPreserved": true, + "serverUpgradedBeforeLegacyClient": true, + "serverVersionReadbacks": 6, + "recovery": { + "point": "first-private-ledger", + "actualTargetEntrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "actualParentSIGKILL": true, + "actualOrphanSIGKILL": true, + "actualOldPublicBootstrapRecovery": true, + "completedControlThroughout": true + } + }, + "providerCaptureTested": "constructed native-format Codex source through genuine installed readers; actual Claude packages installed and admitted, Claude live source not exercised", + "realOSRegistrationTested": false, + "rejectedReadOnlyManagerProbes": 1, + "server": "owned authenticated HTTP Test Adapter; no deployed Instance touched" + } + }, + { + "case": "before-readiness", + "elapsedMilliseconds": 32182, + "required": true, + "result": "passed", + "evidence": { + "baselineVersion": "0.5.10", + "baselineIntegrities": { + "@atape/cli": "sha512-edQJ66nr0VMaKgxYaRDOCiSwJeueEVRTRtBQFk815skIPt7sBOR8ZCfFNhMhOHQ+nU/iY8BEsUxQHoO/LPSCJQ==", + "@atape/adapter-codex": "sha512-tgiD29qMmiH+3DlXtm94XOYBZlVT/N8uXTgX739r9FruGG0Xi4T4M705xbNmhpnrigH9KtzLmyEinEaVHMvfKA==", + "@atape/adapter-claude": "sha512-VDtK5uXgp2vSyhfqKNedpDxFa91R4nfJgrOXH2Hs0O16LhT7RS1dKxiFPhao1q3UReD31xHPnXQcmC8v4oUWYw==" + }, + "baselineEntrySha256": "a1c8ca45d6b338e312b3bb7fe3a40126166d9208278d04a9d3b460537c8f656d", + "bridgeVersion": "0.5.11", + "bridgeEntrySha256": "c1fe6589fc5a04a3dbcfca6d1dbe4cdd64c9736551d3aa7a385470a987bae4f7", + "bridgeCLIIntegrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "bridgeBundle": { + "protocol": "atape.release-bundle.v2", + "version": "0.5.11", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "migration": { + "protocol": "atape.capture-migration.v1", + "id": "journal-v7-to-v8", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ] + }, + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.11.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-fgkgFrNltDa6+Vq/0+SE4xmqvCyPL1SdYgP0KIDu/uas2Oz5OyLly/562zzMkfKfymuFCZUGALqzMMbQ8BLw9Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.11.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-WD05HLDZyZg+24SAm9MAjk0Ob+tMoyuzI5Kk/teESNzWNX5aN7X8sCLAcls7NyaQg4uOmRfWAkclvKAR5CW4YQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-3BbggtxinR210yLzqk1GKaRxbT1aWnnv9Xe6/NWDvzkBh5lWnawva6azCGeF9gKjr7UjZgEUKxihzRgQ9eBqKA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.11.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-y8iAqWqBT+I2E22ofgjVgATIFE9GrMGkCbpSpNw8UwtDx13GyUkijb0YmktcB0sQ7TWr8gmThL9wWeuLlKXacA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.11.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-sKY39aSPOt55XWtHZ6jrLZvcukIMdtfdfjqv+c0p3G1L6EqMZD6ai9mHigy1xcms17Xp/tB5BShPWrvw/PNICw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.11.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-Ggc3vy7laTb2imRSjK6YvbLqKkSbF1Yl7I5v+yI5BxuwzBe4t8DnQg3Yqn9zs2oQi7X9SsxH7HhyrBqy2UAfSA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.11.tgz" + } + ] + }, + "actualPublishedOldAutomaticWorker": true, + "oldManifestDecoderAcceptedActualBridgePackages": true, + "activeCollectorContinued": true, + "wantedPreserved": true, + "bootstrapBytesPreservedAtFirstHandoff": true, + "globalBootstrapBytesPreserved": true, + "globalNPMPrefixReadOnly": true, + "automaticGlobalInstallForbidden": true, + "projectsAndToolsPreserved": true, + "credentialBytesPreserved": true, + "acknowledgedCaptureProgressPreserved": true, + "canonicalBridgeNoReplay": true, + "rawReceiptsPreserved": true, + "appendedContinuation": true, + "stopAndUpdatesOffPreserved": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "actualBaselineClaudeJournalRegistered": true, + "baselineJournalVersion": 8, + "fullChain": "passed-with-controlled-installed-candidate-artifacts", + "feature": { + "version": "0.5.12", + "entrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "cliIntegrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "bundle": { + "protocol": "atape.release-bundle.v3", + "version": "0.5.12", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "updateTargetProtocol": "atape.update-target.v1", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ], + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-s9lJPBMZMMyojIhg0wkGDoICarfMLwrxugdQV/r0qzCz4Zy9lyYVKMiUL939TaB0lij9OX8Zr49DuDDyxVO9WQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.12.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-lPviL9eCfRhWhHLmRy2oJOgxAe/tMVQ3Bkz6UcJK8ucStmxAT9Hw3TDdlPs7HI/tX3EbGmm4qGRL5tRMSt2o4Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.12.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-g6ZbiMuzR/4tmuhGfP+w6420+u57K/k6Sap99OPdS1M9wjWhTEQVuQ9f05UnSwqxqazY9xqaQMycWumuNesxRQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-b8woaxKsz9fqok8tqntCiHi3FaKugV7oW99zOSItXLO5uncAoN9vGAy/NWuotJH2kAmvZ30W7DZtKAptiEOREg==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.12.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-sysne93OUKTvMpWkcXaVdyfZoTK6GqHHFwFAb5jjFjXtUQ1sH0qoVJZBZxZjf2ZLIKEuz7GZSTOxqJ1Vg+DG/w==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.12.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-adVnbre65Ed6IdWNk3riiVeIMx9B9WaHjT4tNcpv0Owah5/3zHuWbcN73wNqAf+fX6wMq0bbQY3+4hDKuVhcjA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.12.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-uizSGMwP4DaNTMqZYAarVXNi6FVy+Sjn9vl75+6KLItXLUCdcr2awBRhqlx256SiIOYPcKAs+RSmdsMsqZDPTQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.12.tgz" + } + ] + }, + "actualBridgeAutomaticWorker": true, + "actualTargetPrepareAndActivate": true, + "nonEmptyCaptureMigrationReceiptVerified": true, + "registeredCaptureAccountsVerified": 1, + "sourceJournalVersion": 8, + "targetJournalVersion": 8, + "targetOwnerVerified": true, + "nativeV5Replay": true, + "eventIDsPreserved": true, + "revisionsAdvanced": true, + "rawReceiptsPreservedAcrossReplay": true, + "nativeFileActivityReplayed": true, + "appendedContinuation": true, + "lateOfflineLegacyRoutesRetained": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "globalBootstrapBytesPreserved": true, + "serverUpgradedBeforeLegacyClient": true, + "serverVersionReadbacks": 6, + "recovery": { + "point": "before-readiness", + "actualTargetEntrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "actualParentSIGKILL": true, + "actualOrphanSIGKILL": true, + "actualOldPublicBootstrapRecovery": true, + "completedControlThroughout": true + } + }, + "providerCaptureTested": "constructed native-format Codex source through genuine installed readers; actual Claude packages installed and admitted, Claude live source not exercised", + "realOSRegistrationTested": false, + "rejectedReadOnlyManagerProbes": 1, + "server": "owned authenticated HTTP Test Adapter; no deployed Instance touched" + } + }, + { + "case": "orphan-apply", + "elapsedMilliseconds": 43776, + "required": true, + "result": "passed", + "evidence": { + "baselineVersion": "0.5.10", + "baselineIntegrities": { + "@atape/cli": "sha512-edQJ66nr0VMaKgxYaRDOCiSwJeueEVRTRtBQFk815skIPt7sBOR8ZCfFNhMhOHQ+nU/iY8BEsUxQHoO/LPSCJQ==", + "@atape/adapter-codex": "sha512-tgiD29qMmiH+3DlXtm94XOYBZlVT/N8uXTgX739r9FruGG0Xi4T4M705xbNmhpnrigH9KtzLmyEinEaVHMvfKA==", + "@atape/adapter-claude": "sha512-VDtK5uXgp2vSyhfqKNedpDxFa91R4nfJgrOXH2Hs0O16LhT7RS1dKxiFPhao1q3UReD31xHPnXQcmC8v4oUWYw==" + }, + "baselineEntrySha256": "a1c8ca45d6b338e312b3bb7fe3a40126166d9208278d04a9d3b460537c8f656d", + "bridgeVersion": "0.5.11", + "bridgeEntrySha256": "c1fe6589fc5a04a3dbcfca6d1dbe4cdd64c9736551d3aa7a385470a987bae4f7", + "bridgeCLIIntegrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "bridgeBundle": { + "protocol": "atape.release-bundle.v2", + "version": "0.5.11", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "migration": { + "protocol": "atape.capture-migration.v1", + "id": "journal-v7-to-v8", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ] + }, + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-SYZQnSmI9QSKnzEWumIIE22saB919cA2ebGJzCGfnaI4lDLiGLA4rW9c605464B7SBhNfY8zUV0C6Gvsj2rCZA==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-b3h4/RBl8GbNUN/M2IxkjnV/IujJqOLl1z99C9w6mzP499fENSU08SQbjXRmEephe05J92JmcfkPZEcd6wlLDw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.11.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-fgkgFrNltDa6+Vq/0+SE4xmqvCyPL1SdYgP0KIDu/uas2Oz5OyLly/562zzMkfKfymuFCZUGALqzMMbQ8BLw9Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.11.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-WD05HLDZyZg+24SAm9MAjk0Ob+tMoyuzI5Kk/teESNzWNX5aN7X8sCLAcls7NyaQg4uOmRfWAkclvKAR5CW4YQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.11.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-3BbggtxinR210yLzqk1GKaRxbT1aWnnv9Xe6/NWDvzkBh5lWnawva6azCGeF9gKjr7UjZgEUKxihzRgQ9eBqKA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.11.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-y8iAqWqBT+I2E22ofgjVgATIFE9GrMGkCbpSpNw8UwtDx13GyUkijb0YmktcB0sQ7TWr8gmThL9wWeuLlKXacA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.11.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-sKY39aSPOt55XWtHZ6jrLZvcukIMdtfdfjqv+c0p3G1L6EqMZD6ai9mHigy1xcms17Xp/tB5BShPWrvw/PNICw==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.11.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-Ggc3vy7laTb2imRSjK6YvbLqKkSbF1Yl7I5v+yI5BxuwzBe4t8DnQg3Yqn9zs2oQi7X9SsxH7HhyrBqy2UAfSA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.11.tgz" + } + ] + }, + "actualPublishedOldAutomaticWorker": true, + "oldManifestDecoderAcceptedActualBridgePackages": true, + "activeCollectorContinued": true, + "wantedPreserved": true, + "bootstrapBytesPreservedAtFirstHandoff": true, + "globalBootstrapBytesPreserved": true, + "globalNPMPrefixReadOnly": true, + "automaticGlobalInstallForbidden": true, + "projectsAndToolsPreserved": true, + "credentialBytesPreserved": true, + "acknowledgedCaptureProgressPreserved": true, + "canonicalBridgeNoReplay": true, + "rawReceiptsPreserved": true, + "appendedContinuation": true, + "stopAndUpdatesOffPreserved": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "actualBaselineClaudeJournalRegistered": true, + "baselineJournalVersion": 8, + "fullChain": "passed-with-controlled-installed-candidate-artifacts", + "feature": { + "version": "0.5.12", + "entrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "cliIntegrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "bundle": { + "protocol": "atape.release-bundle.v3", + "version": "0.5.12", + "captureStateContract": "atape.client.v3-capture.v2", + "updateControlProtocol": "atape.update-control.v1", + "updateTargetProtocol": "atape.update-target.v1", + "fromCaptureStateContracts": [ + "atape.client.v3-capture.v1", + "atape.client.v3-capture.v2" + ], + "packages": [ + { + "name": "@atape/cli", + "integrity": "sha512-jAn1iUKbt+eUOgiYN41fEyrFBlLroaWytsJG2+BAguvBUuU+t/En/jqfaYq3CIuz4lAeib1+ht7Zpom586weCQ==", + "tarball": "https://registry.npmjs.org/@atape/cli/-/cli-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codex", + "integrity": "sha512-s9lJPBMZMMyojIhg0wkGDoICarfMLwrxugdQV/r0qzCz4Zy9lyYVKMiUL939TaB0lij9OX8Zr49DuDDyxVO9WQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codex/-/adapter-codex-0.5.12.tgz" + }, + { + "name": "@atape/adapter-claude", + "integrity": "sha512-lPviL9eCfRhWhHLmRy2oJOgxAe/tMVQ3Bkz6UcJK8ucStmxAT9Hw3TDdlPs7HI/tX3EbGmm4qGRL5tRMSt2o4Q==", + "tarball": "https://registry.npmjs.org/@atape/adapter-claude/-/adapter-claude-0.5.12.tgz" + }, + { + "name": "@atape/adapter-opencode", + "integrity": "sha512-g6ZbiMuzR/4tmuhGfP+w6420+u57K/k6Sap99OPdS1M9wjWhTEQVuQ9f05UnSwqxqazY9xqaQMycWumuNesxRQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-opencode/-/adapter-opencode-0.5.12.tgz" + }, + { + "name": "@atape/adapter-codebuddy", + "integrity": "sha512-b8woaxKsz9fqok8tqntCiHi3FaKugV7oW99zOSItXLO5uncAoN9vGAy/NWuotJH2kAmvZ30W7DZtKAptiEOREg==", + "tarball": "https://registry.npmjs.org/@atape/adapter-codebuddy/-/adapter-codebuddy-0.5.12.tgz" + }, + { + "name": "@atape/adapter-kimi", + "integrity": "sha512-sysne93OUKTvMpWkcXaVdyfZoTK6GqHHFwFAb5jjFjXtUQ1sH0qoVJZBZxZjf2ZLIKEuz7GZSTOxqJ1Vg+DG/w==", + "tarball": "https://registry.npmjs.org/@atape/adapter-kimi/-/adapter-kimi-0.5.12.tgz" + }, + { + "name": "@atape/adapter-grok", + "integrity": "sha512-adVnbre65Ed6IdWNk3riiVeIMx9B9WaHjT4tNcpv0Owah5/3zHuWbcN73wNqAf+fX6wMq0bbQY3+4hDKuVhcjA==", + "tarball": "https://registry.npmjs.org/@atape/adapter-grok/-/adapter-grok-0.5.12.tgz" + }, + { + "name": "@atape/adapter-cursor", + "integrity": "sha512-uizSGMwP4DaNTMqZYAarVXNi6FVy+Sjn9vl75+6KLItXLUCdcr2awBRhqlx256SiIOYPcKAs+RSmdsMsqZDPTQ==", + "tarball": "https://registry.npmjs.org/@atape/adapter-cursor/-/adapter-cursor-0.5.12.tgz" + } + ] + }, + "actualBridgeAutomaticWorker": true, + "actualTargetPrepareAndActivate": true, + "nonEmptyCaptureMigrationReceiptVerified": true, + "registeredCaptureAccountsVerified": 1, + "sourceJournalVersion": 8, + "targetJournalVersion": 8, + "targetOwnerVerified": true, + "nativeV5Replay": true, + "eventIDsPreserved": true, + "revisionsAdvanced": true, + "rawReceiptsPreservedAcrossReplay": true, + "nativeFileActivityReplayed": true, + "appendedContinuation": true, + "lateOfflineLegacyRoutesRetained": true, + "retainedOldBootstrapRejectedUnknownCaptureLedger": true, + "globalBootstrapBytesPreserved": true, + "serverUpgradedBeforeLegacyClient": true, + "serverVersionReadbacks": 6, + "recovery": { + "point": "orphan-apply", + "actualTargetEntrySha256": "dba6d87cc0fc533fb6e97806108bf05b705598ecbbba51a99bdd76db115b0483", + "actualParentSIGKILL": true, + "actualOrphanSIGKILL": true, + "actualOldPublicBootstrapRecovery": true, + "completedControlThroughout": true, + "queuedOldWorker": { + "actualEntrySha256": "a1c8ca45d6b338e312b3bb7fe3a40126166d9208278d04a9d3b460537c8f656d", + "attemptSuperseded": true, + "orphanApplyExclusionPreserved": true, + "exit": 1 + }, + "stopPreservedAcrossRecovery": true + } + }, + "providerCaptureTested": "constructed native-format Codex source through genuine installed readers; actual Claude packages installed and admitted, Claude live source not exercised", + "realOSRegistrationTested": false, + "rejectedReadOnlyManagerProbes": 2, + "server": "owned authenticated HTTP Test Adapter; no deployed Instance touched" + } + } + ], + "scope": "All five independent cases use actual installed immutable npm .10/.11 and final checksummed .12 packages. The HTTP Test Adapter advertises Server/minimum CLI .12 from the first old-client connection. Genuine old global entry and read-only prefix are preserved. Each case directly binds a nonempty genuine Claude SourceCapture journal inventory to the completed target receipt and owner; the observed source and final formats are recorded, without assuming a 7-to-8 conversion. The separate untouched installed candidate contract exercises real controlled SQL7-to-SQL8 migration. Controlled native source and external HTTP/OS Test Adapters do not establish live Provider or OS registration acceptance." + }, + "artifacts": [ + { + "filename": "atape-adapter-claude-0.5.12.tgz", + "bytes": 159945, + "sha256": "044e331e0301987b9b0e146824f4e87a311a65b5b071cb6226eb6780af7e5f28" + }, + { + "filename": "atape-adapter-codebuddy-0.5.12.tgz", + "bytes": 153108, + "sha256": "c1e5739bd1d25383caae7abadae6f52f1dbec2cc39388b1130072eaa3e76879b" + }, + { + "filename": "atape-adapter-codex-0.5.12.tgz", + "bytes": 161307, + "sha256": "f603d1fe55ccfd0c62e3f03dfa2c7e2e193224ebdba435f38a715f8cb9e29f98" + }, + { + "filename": "atape-adapter-cursor-0.5.12.tgz", + "bytes": 151178, + "sha256": "b57f0edb11a91b249e806ec847859cecb7b0b842bc49348b28bd7464073ad62f" + }, + { + "filename": "atape-adapter-grok-0.5.12.tgz", + "bytes": 147232, + "sha256": "930a84ebd0b65a6ebb144cfa20db6819ee0644350368fa3de867e61dd05e4241" + }, + { + "filename": "atape-adapter-kimi-0.5.12.tgz", + "bytes": 151242, + "sha256": "63c562338da351de480d3a75cf98c3706a837c767cf700d790652f37aeb690d3" + }, + { + "filename": "atape-adapter-opencode-0.5.12.tgz", + "bytes": 150792, + "sha256": "edbdfee9ab28419a643e55711f5ea10bf790e865e29d7576ed948ac5e244cbdf" + }, + { + "filename": "atape-cli-0.5.12.tgz", + "bytes": 734778, + "sha256": "fb4e4995cb8c69b7ae13a5b9816dca6b77595c5b68a349297168c57cf41a1f33" + } + ], + "checksums": { + "filename": "SHA256SUMS", + "sha256": "02c2e7ff2867c2f91edd2d8baa3052d9dfdc5bc9aa640e55b529cc52c05bab8d" + }, + "manualStaging": { + "status": "pending", + "unverifiedChecks": [ + "candidate-artifacts", + "official-topology", + "github-provider-staging-smoke", + "official-domain-egress-deny-smoke", + "two-user-two-team", + "waf-tls-callback", + "backup-restore", + "observability-secret-sample", + "rollback", + "release-signoff" + ], + "authorization": "The explicit 2026-10-12 Asia/Singapore npm publication request authorizes the candidate-bound manual-only waiver under ADR-0048. No automated gate is waived." + }, + "limits": [ + "Native Windows automatic update lifecycle has not been executed. Its native-only acquisition caller test is explicitly skipped on this macOS host; required POSIX cases cannot skip.", + "Live Claude/Cursor sessions, real GitHub OAuth, native login/wake registration and complete manual staging are unverified.", + "Missing source history cannot be reconstructed. File facts remain bounded explicit recorded evidence; search scope and failed edits are not successful filesystem changes.", + "Byte identity and decoded provenance content do not establish independent cryptographic signature verification.", + "Publication, production ARM image/security checks, matched backup/snapshot and Server deployment are separate subsequent actions.", + "Existing discovery cache and 24–30 hour automatic checks mean publication does not prove immediate client uptake.", + "Canonical/publication v5 creates a durable format boundary even with unchanged SQL24; image-only downgrade to a pre-v5 Server is unsafe after v5 writes." + ] +} diff --git a/docs/releases/v0.5.12.md b/docs/releases/v0.5.12.md new file mode 100644 index 0000000..cd35991 --- /dev/null +++ b/docs/releases/v0.5.12.md @@ -0,0 +1,125 @@ +# ATape v0.5.12: Recorded file activity + +This coordinated target-owned implementation contains the CLI and seven official +Adapters at 0.5.12, with matching Web, Server and container metadata. +Authentication remains `auth-v1`, including the 180 days absolute Web Session +lifetime. Release and minimum CLI metadata are 0.5.12. Implementation, +integration, npm publication and Server deployment are separate actions. + +## Recorded tool and file activity + +Codex and Claude emit Canonical/publication v5 facts for explicit native +tool identities and read, edit and search paths. Analytics v4 groups tools by +stable identity and reports exact recorded paths, distinct calls, operation +counts and coverage. File and operation filters open the final original tool +update through a conditional Reader link; browser Back restores the selection. + +Paths retain their recorded spelling. Search paths describe scope, rather than +matching files. An edit invocation does not prove a successful filesystem +change. Unknown, invalid, oversized and redacted facts remain unavailable; +commands, output lists and patch bodies do not supply guessed paths. Upload +preparation clears copied path facts when their original sensitive fields are +masked. No Raw or Search data enters the analysis report. + +Available source history replays with stable Event IDs and higher revisions. +Codex uses projection/cursor v7; Claude uses source profile active-path.4 and +projection v7. Frozen pending publications and Raw acknowledgements retain +their original bytes. Unavailable history remains unrepaired. Other official +Adapters retain their existing capture scope and do not infer these new facts. + +## Automatic delivery and recovery + +Publication policy keeps the genuine 0.5.11 bridge on npm/GitHub Latest and +retains legacy catalog v1/v2 routes. The 0.5.12 release is assigned npm +`atape-managed` and catalog v3 only. A long-offline 0.5.10 client can first +receive that real bridge, then hand target +preparation, migration and recovery to this verified runtime. Normal updates +preserve the global npm entry and do not require prefix write permission. + +The 0.5.12 manual CLI operation also preserves the global entry for catalog v3, +including before tool initialization. It still performs complete-bundle target +preparation and activation. A current managed runtime does not repeatedly offer +an update solely because its global entry is older. Historical v1/v2 entry +replacement remains available for their compatibility transitions. + +The already published 0.5.11 manual menu can report an entry-refresh failure +after selecting the new runtime on a read-only npm prefix. Its immutable bytes +retain this limitation. Installed unattended acceptance therefore executes the +genuine 0.5.11 automatic worker; the new manual policy is implemented in 0.5.12. + +The target-owned Interface retains writer exclusion, bounded durable receipts, +Stop intent and updates-off preference. Interrupted handoffs recover forward +through the exact recorded target before reading its private state. Historical +0.5.9 cannot cross the clock transition unattended. It still requires the +documented stopped entry replacement into the compatible 0.5.11 bridge before +the target-owned feature update. This release cannot change that published +old decoder. Native Windows update lifecycle and live Claude/Cursor acceptance +are outside POSIX installed acceptance. + +The [bridge delivery record](evidence/v0.5.11-delivery.json) confirms genuine +0.5.11 publication and independent anonymous public-byte verification. It does +not claim publication or deployment of this feature release. + +See [Session Analytics](../session-analytics.md), +[target-owned updates](../cli/setup-and-adapters.md#target-owned-updates) and +[ADR-0126](../architecture/adr/0126-target-owned-managed-updates.md). + +## Server and durable-data boundary + +Deploy a v5-capable Server before advertising this feature release. The SQL +schema remains version 24; accepting v5 data still creates a durable format +boundary. A pre-v5 Server must not be restored through an image switch after +v5 writes. Retain the matched PostgreSQL/Raw backup and use compatible forward +repair. A deliberate paired restore withdraws later writes and needs a separate +operator decision. + +## Acceptance and remaining limits + +Local implementation verification passed repository checks, production builds, +release-policy checks, dependency checks at the configured security threshold, +Go race/fuzz tests, PostgreSQL integration, self-hosting configuration and a +paired PostgreSQL/Raw restore rehearsal. The installed HTTP/provider corpus +passed in 655.429 seconds; the separate authentication maintenance contract +passed in 7.65 seconds. Local candidate container builds and Web rollout unit +tests also passed. + +The final checksummed packages passed required installed compatibility through +the genuine published 0.5.10 → 0.5.11 → 0.5.12 automatic chain and four interruption +cases: control synced, first private ledger, before readiness and orphan apply. +Each case verifies an actual registered Claude journal, its complete target-bound +receipt and owner. The observed journal format is 8 before and after this update; +the separate installed candidate contract verifies controlled SQL7 → SQL8 +migration. The historical 0.5.9 stopped transition reaches the genuine bridge, +then its automatic worker reaches 0.5.12 while retaining Stop intent. The test +explicitly enables automatic updates for that second leg and disables them at +the end. Manual v3 policy also passed 39 application caller tests. + +The [candidate acceptance record](evidence/v0.5.12-update-acceptance.json) retains +artifact hashes, completed checks and the earlier failed attempts with their +repairs. These remain local candidate checks. Exact-head CI and image Security +are mandatory before integration and publication. Package publication, public +byte verification, Server deployment and client uptake are separate delivery +actions; their records establish when they have completed. + +The user's explicit package publication request includes the incomplete manual +staging waiver under ADR-0048. Automated CI, integration, installed +compatibility and security gates remain mandatory. The following manual checks +remain unverified: + +- `candidate-artifacts` +- `official-topology` +- `github-provider-staging-smoke` +- `official-domain-egress-deny-smoke` +- `two-user-two-team` +- `waf-tls-callback` +- `backup-restore` +- `observability-secret-sample` +- `rollback` +- `release-signoff` + +Constructed native source and synthetic authenticated API acceptance do not +establish a fresh live Provider session, real GitHub OAuth, native login/wake +registration or complete manual staging. Public byte identity and decoded +provenance content do not establish independent cryptographic signature +verification. Publication does not prove immediate client uptake: existing +discovery cache and automatic-check scheduling still apply. diff --git a/docs/releasing.md b/docs/releasing.md index d0d3b84..030dcdf 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -137,6 +137,21 @@ frozen checksummed directory without building or repacking: node apps/cli/scripts/codex-clock-release-gate.mjs /absolute/frozen/tarballs ``` +For a target-owned release, this historical clock gate first performs the +stopped 0.5.9 transition into the genuine retained 0.5.11 bridge. In the same +disposable installation, the test explicitly enables automatic updates and +dispatches the genuine installed bridge worker to the candidate's v3-only +target-owned bundle. An owned clock Test Adapter expires the ordinary discovery +cache and retry schedule without editing private updater state. Both legs verify +replay, stable source Event IDs, higher revisions, Raw receipts, Stop intent, +no-op and append. The first leg preserves updates-off; the second preserves the +explicitly enabled preference, then the test explicitly disables it. The second +leg preserves the bridge's global entry and executes preparation, activation and +collection in the exact installed candidate. It never advertises a legacy v1/v2 +descriptor for the feature release. The published 0.5.11 manual menu's global +refresh limitation remains recorded; this gate does not claim that menu succeeded. +This does not make the historical 0.5.9 clock boundary unattended. + The exact checksummed OpenCode, CodeBuddy, Kimi, Grok and Cursor artifacts also run their installed source-capability verification outside the checkout with controlled native data. The workflow publishes the exact release tarballs and attaches them plus `SHA256SUMS` to the GitHub Release. @@ -299,6 +314,26 @@ their old route. Installed acceptance must exercise authentic 0.5.10 automatic delivery to the real bridge and then the real feature artifact. A stopped manual replacement or controlled dispatcher test is not that evidence. +For the 0.5.12 target-owned POSIX release, the formal `test:release` caller +anonymously acquires all eight genuine 0.5.11 bridge archives with fixed +published SHA-512 integrity, plus the three genuine 0.5.10 baseline archives. +Acquisition retains the 8 MiB per-archive bound and 30-second deadline. It reuses +those bytes across five independent installed cases and verifies their fixed +identity again for each case: ordinary delivery, public-control sync interruption, +first private ledger interruption, pre-readiness interruption and orphan apply +exclusion. Every required case must complete; there is no optional environment +skip. The owned Server HTTP Test Adapter advertises the feature Server from the +old client's first connection, covering Server-first sequencing separately from +real Go deployment. Windows retains the explicit POSIX lifecycle limitation. + +Each case also verifies the genuine baseline Claude SourceCapture account's +registry, binding and SQLite file, then binds its nonempty inventory to the +completed target receipt and owner. Record the observed source and target +journal formats; a completed receipt for an existing format-8 account does not +claim a 7-to-8 conversion. The separate installed candidate migration contract +exercises controlled SQL7-to-SQL8 data. Claude discovery remains empty in the +automatic-chain fixture, so this does not establish a live Claude session. + The bridge acceptance must keep the genuine historical global entry unchanged and prove that target-owned updates need no npm-prefix write permission. Include interruption after the completed execution handoff and before private migration diff --git a/docs/session-analytics.md b/docs/session-analytics.md index d0f0a85..a2cf5e4 100644 --- a/docs/session-analytics.md +++ b/docs/session-analytics.md @@ -5,14 +5,14 @@ from the current authorized Canonical Session. The Go Module owns metric rules; the HTTP Adapter exposes its `Open` Interface. The analysis view connects statistics, normalized tool identities, recorded activity ranges and conditional reader evidence links. This -guide describes checkout behavior and does not claim publication or deployment. -The native-tool and file-activity increments implement the unpublished -Canonical/publication v5 and Analytics v4 contracts. The published/deployed -0.5.10 Server does not implement these contracts. The 0.5.11 release candidate -is an automatic-update bridge: its Host, Server and reader retain v5 support, -while official Codex/Claude Adapters keep their actual older v3/v4 behavior. -The subsequent target-owned feature release restores v5 provider projection. -See [target-owned updates](cli/setup-and-adapters.md#target-owned-updates). +guide describes implemented behavior. The [0.5.12 release notes](releases/v0.5.12.md) +own publication, deployment and acceptance evidence. +The 0.5.12 target-owned implementation restores native-tool and file-activity +projection in the official Codex/Claude Adapters through Canonical/publication +v5 and Analytics v4. The 0.5.11 bridge already includes supporting Host, Server +and reader behavior while retaining genuine older v3/v4 provider output. +Published 0.5.10 does not implement these contracts. See +[target-owned updates](cli/setup-and-adapters.md#target-owned-updates). ## Metrics and evidence @@ -35,8 +35,8 @@ and `incompleteSamples` describe recorded samples, not completeness of the sourc conversation. Session and Thread `captureStatus` retain partial-capture disclosure. No work time, tool execution duration, cost, productivity score, inferred outcome or generated summary is provided. Record span describes the first and last -eligible stored source clocks. The bridge's Codex projection v5 corrects -fabricated clocks and retains v4's historical missing-status correction +eligible stored source clocks. Codex projection v7 inherits projection v5's +correction of fabricated clocks and v4's historical missing-status correction on successful source replay. Unreplayed historical records can still carry an older Adapter's epoch or outcome; saved non-null clock coverage alone does not establish clock quality. See @@ -222,15 +222,15 @@ corpus verifies other capture behavior but contains no corresponding MCP or Skill invocation; a fresh recorded live invocation has not been validated. That earlier acceptance does not establish the new v5 capture transition. -The later v5 Claude capture requires explicit Host Canonical v5 support and a +Current Claude capture requires explicit Host Canonical v5 support and a Server advertising publication target v5. V5 Codex paged capture requires explicit Host and Server Canonical v5 support. The published 0.5.10 Server does not advertise either v5 path. Unsupported Hosts/Servers refuse before source reads or progress writes; they do not silently downgrade identities. Upgrade the Server and CLI/Adapter together when a supporting release is available. -In that feature release, available source history can replay with stable Event -IDs and higher projection revisions. Its Codex cursor is v7; an older Adapter cannot read it. Recovery +Available source history can replay with stable Event IDs and higher projection +revisions. The Codex cursor is v7; an older Adapter cannot read it. Recovery continues forward using a compatible runtime and retained progress, rather than deleting checkpoints. Claude's new projection similarly requires a compatible reader. Missing source history remains unidentified. Already frozen accepted @@ -239,8 +239,8 @@ See [the v5 ingestion contract](api/canonical-ingestion.md#v5-stable-native-tool and [the upgrade boundary](cli/setup-and-adapters.md#stable-native-tool-identity-checkout-increment). No compatibility is claimed between intermediate development-checkout v5/v4 -artifacts. The first target-owned v5 feature release must include the complete -file-activity contract; the bridge does not produce it. +artifacts. The 0.5.12 target-owned implementation includes the complete +file-activity contract; the 0.5.11 bridge does not produce it. The earlier v4 source-metadata compatibility encoding and its genuine 0.5.8 recovery evidence apply only to v4 captures. V5 recovery metadata carries explicit diff --git a/package.json b/package.json index 7e5b0d8..299363c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "atape", - "version": "0.5.11", + "version": "0.5.12", "private": true, "license": "MIT", "packageManager": "pnpm@11.7.0", diff --git a/packages/application/src/cliUpgrade.test.ts b/packages/application/src/cliUpgrade.test.ts index a1263c8..9d625d9 100644 --- a/packages/application/src/cliUpgrade.test.ts +++ b/packages/application/src/cliUpgrade.test.ts @@ -11,6 +11,8 @@ const bundle = (version: string, integrity = `sha512-${"A".repeat(86)}==`): Rele packages: releasePackageNames.map(name => ({ name, integrity, tarball: `https://registry.npmjs.org/${name}/-/${name.slice("@atape/".length)}-${version}.tgz` })) }) +const targetBundle = (version: string): ReleaseBundle => ({ ...bundle(version), protocol: "atape.release-bundle.v3", + updateTargetProtocol: "atape.update-target.v1", fromCaptureStateContracts: ["atape.client.v3-capture.v1", "atape.client.v3-capture.v2"] }) const fixture = (version = "0.4.2", running = true, wanted = running, installed = "0.4.1", selected = bundle(version)) => { let failInstall = false, failResume = false, installs = 0, pauses = 0, stale = false let stopDuringActivation = false @@ -94,7 +96,7 @@ const fixture = (version = "0.4.2", running = true, wanted = running, installed } return { run: (effect: Effect.Effect>, signal?: AbortSignal) => Effect.runPromise(effect.pipe(Effect.provide(layer)), signal ? { signal } : undefined), ownership: () => ({ owned, acquisitions, releases }), hold, - prepared, activations, entryBundles, runtimeVersion: () => runtimeVersion, config: () => structuredClone(config), + prepared, activations, entryBundles, runtimeVersion: () => runtimeVersion, commandEntryVersion: () => installed, config: () => structuredClone(config), leases: () => leases, preparationReleases: () => preparationReleases, edit: (change: (config: ClientConfig) => ClientConfig) => { config = change(config) }, configure: (value: boolean) => { config = { ...config, toolsConfigured: value, ...(value ? {} : { adapters: [] }) } }, @@ -201,12 +203,10 @@ describe("CLI upgrade Module", () => { expect(client.config()).toEqual(before) }) - it.each(["atape.release-bundle.v2", "atape.release-bundle.v3"] as const)("settles %s before refreshing an unconfigured command entry", async protocol => { - const selected: ReleaseBundle = protocol === "atape.release-bundle.v2" - ? { ...bundle("0.4.2"), protocol, migration: { protocol: "atape.capture-migration.v1", id: "journal-v7-to-v8", + it("settles an explicit v2 capture migration before refreshing an unconfigured command entry", async () => { + const selected: ReleaseBundle = { ...bundle("0.4.2"), protocol: "atape.release-bundle.v2", + migration: { protocol: "atape.capture-migration.v1", id: "journal-v7-to-v8", fromCaptureStateContracts: ["atape.client.v3-capture.v1", "atape.client.v3-capture.v2"] } } - : { ...bundle("0.4.2"), protocol, updateTargetProtocol: "atape.update-target.v1", - fromCaptureStateContracts: ["atape.client.v3-capture.v1", "atape.client.v3-capture.v2"] } for (const current of ["0.4.1", "0.4.2"]) { const client = fixture("0.4.2", false, false, "0.4.1", selected) client.configure(false) @@ -227,6 +227,73 @@ describe("CLI upgrade Module", () => { expect(refused.starts).toEqual([]) }) + it.each([true, false])("activates a complete target-owned release with sync running=%s while the global prefix is unwritable", async running => { + const selected = targetBundle("0.5.13"), client = fixture("0.5.13", running, running, "0.5.11", selected) + client.edit(config => ({ ...config, adapters: config.adapters.map(adapter => ({ ...adapter, version: "0.5.12" })) })) + client.failInstall() + const before = client.config() + expect(await client.run(upgradeCLI("0.5.12"))).toEqual({ version: "0.5.13", updated: true, resumed: running }) + expect(client.prepared).toEqual([{ bundle: selected, adapters: before.adapters, automatic: false }]) + expect(client.activations).toEqual([{ bundle: selected, automatic: false }]) + expect(client.commandEntryVersion()).toBe("0.5.11") + expect(client.entryBundles).toEqual([]) + expect(client.runtimeVersion()).toBe("0.5.13") + expect(client.running()).toBe(running) + expect(client.pauses()).toBe(0); expect(client.starts).toEqual([]) + expect(client.leases()).toBe(0); expect(client.preparationReleases()).toBe(1) + expect(client.ownership()).toEqual({ owned: false, acquisitions: 1, releases: 1 }) + }) + + it("completes an unconfigured target-owned handoff and does not repeat it for the retained older bootstrap", async () => { + const selected = targetBundle("0.5.13"), client = fixture("0.5.13", false, false, "0.5.11", selected) + client.configure(false); client.failInstall() + const before = client.config() + expect(await client.run(upgradeCLI("0.5.12"))).toEqual({ version: "0.5.13", updated: true, resumed: false }) + expect(client.prepared).toEqual([{ bundle: selected, adapters: [], automatic: false }]) + expect(client.activations).toEqual([{ bundle: selected, automatic: false }]) + expect(client.config()).toEqual(before) + expect(await client.run(checkCLIUpgrade("0.5.13"))).toBeUndefined() + expect(await client.run(upgradeCLI("0.5.13"))).toEqual({ version: "0.5.13", updated: false, resumed: false }) + expect(client.prepared).toHaveLength(1); expect(client.activations).toHaveLength(1) + expect(client.commandEntryVersion()).toBe("0.5.11"); expect(client.entryBundles).toEqual([]) + expect(client.starts).toEqual([]) + }) + + it("preserves Stop during target-owned activation without refreshing the global entry or resuming sync", async () => { + const client = fixture("0.5.13", true, true, "0.5.11", targetBundle("0.5.13")) + client.stopDuringActivation(); client.failInstall() + expect(await client.run(upgradeCLI("0.5.12"))).toEqual({ version: "0.5.13", updated: true, resumed: false }) + expect(client.activations).toHaveLength(1); expect(client.installs()).toBe(0) + expect(client.running()).toBe(false); expect(client.starts).toEqual([]) + }) + + it("aligns older official Adapters at the current target-owned runtime without replacing its bootstrap", async () => { + const selected = targetBundle("0.5.13"), client = fixture("0.5.13", false, false, "0.5.11", selected) + client.edit(config => ({ ...config, adapters: config.adapters.map(adapter => ({ ...adapter, version: "0.5.12" })) })) + client.failInstall() + const before = client.config() + expect(await client.run(upgradeCLI("0.5.13"))).toEqual({ version: "0.5.13", updated: true, resumed: false }) + expect(client.prepared).toEqual([{ bundle: selected, adapters: before.adapters, automatic: false }]) + expect(client.activations).toEqual([{ bundle: selected, automatic: false }]) + expect(client.config().adapters[0]?.version).toBe("0.5.13"); expect(client.entryBundles).toEqual([]) + }) + + it("does not bypass target-owned preparation failure even without command-entry replacement", async () => { + const client = fixture("0.5.13", true, true, "0.5.11", targetBundle("0.5.13")), before = client.config() + client.failPrepare(); client.failInstall() + await expect(client.run(upgradeCLI("0.5.12"))).rejects.toMatchObject({ reason: "install", message: expect.stringContaining("could not be prepared") }) + expect(client.prepared).toHaveLength(1); expect(client.activations).toEqual([]) + expect(client.config()).toEqual(before); expect(client.entryBundles).toEqual([]) + expect(client.running()).toBe(true) + }) + + it("advertises a newer target-owned runtime but does not advertise a same-version global entry refresh", async () => { + const client = fixture("0.5.13", false, false, "0.5.11", targetBundle("0.5.13")) + expect(await client.run(checkCLIUpgrade("0.5.12"))).toBe("0.5.13") + expect(await client.run(checkCLIUpgrade("0.5.13"))).toBeUndefined() + expect(client.prepared).toEqual([]); expect(client.entryBundles).toEqual([]) + }) + it("upgrades and resumes only previously running sync with its original settings", async () => { const client = fixture() expect(await client.run(upgradeCLI("0.4.1"))).toEqual({ version: "0.4.2", updated: true, resumed: true }) diff --git a/packages/application/src/cliUpgrade.ts b/packages/application/src/cliUpgrade.ts index d591bc4..bd07b55 100644 --- a/packages/application/src/cliUpgrade.ts +++ b/packages/application/src/cliUpgrade.ts @@ -32,7 +32,7 @@ export const checkCLIUpgrade = Effect.fn("CLIUpgrade.check")(function*(current: return yield* platform.latest(true).pipe( Effect.flatMap(decodeBundle), Effect.flatMap(bundle => newer(current, bundle.version) ? Effect.succeed(undefined) : - newer(bundle.version, current) ? Effect.succeed(bundle.version) : platform.installedVersion().pipe( + newer(bundle.version, current) ? Effect.succeed(bundle.version) : bundle.protocol === "atape.release-bundle.v3" ? Effect.succeed(undefined) : platform.installedVersion().pipe( Effect.map(installed => stableVersion(installed) && newer(bundle.version, installed) ? bundle.version : undefined))), Effect.catch(() => Effect.succeed(undefined)) ) @@ -55,9 +55,12 @@ export const upgradeCLI = Effect.fn("CLIUpgrade.upgrade")(function*(current: str if (adapters.some(adapter => !stableVersion(adapter.version) || newer(adapter.version, version))) { return yield* new CLIUpgradeError({ reason: "installation", message: "An official Adapter is ahead of this release. Reopen ATape and check versions again." }) } - const updateEntry = newer(version, installed) + // Target-owned releases retain the verified npm bootstrap and complete their + // executable/capture handoff inside ATAPE_HOME. Legacy releases still replace + // the command entry through their historical installation path. + const updateEntry = bundle.protocol !== "atape.release-bundle.v3" && newer(version, installed) // Both explicit migration and target-owned releases must settle retained - // capture state after tools are disconnected, before replacing the npm entry. + // capture state after tools are disconnected through the complete coordinator. const managedActivation = bundle.protocol === "atape.release-bundle.v2" || bundle.protocol === "atape.release-bundle.v3" const updateRuntime = (config.toolsConfigured || managedActivation) && (newer(version, current) || adapters.some(adapter => adapter.version !== version) || (managedActivation && updateEntry)) diff --git a/packages/application/src/toolUpdates.test.ts b/packages/application/src/toolUpdates.test.ts index 967e48e..0b7461d 100644 --- a/packages/application/src/toolUpdates.test.ts +++ b/packages/application/src/toolUpdates.test.ts @@ -11,13 +11,15 @@ const bundle = (version: string): ReleaseBundle => ({ protocol: "atape.release-b captureStateContract: "atape.client.v3-capture.v2", updateControlProtocol: "atape.update-control.v1", packages: releasePackageNames.map(name => ({ name, integrity: `sha512-${"A".repeat(86)}==`, tarball: `https://registry.npmjs.org/${name}/-/${name.slice("@atape/".length)}-${version}.tgz` })) }) +const targetBundle = (version: string): ReleaseBundle => ({ ...bundle(version), protocol: "atape.release-bundle.v3", + updateTargetProtocol: "atape.update-target.v1", fromCaptureStateContracts: ["atape.client.v3-capture.v1", "atape.client.v3-capture.v2"] }) -const fixture = (runtimeReleaseVersion = "0.4.4", commandEntryVersion = "0.4.4") => { +const fixture = (runtimeReleaseVersion = "0.4.4", commandEntryVersion = "0.4.4", selected = bundle("0.4.4")) => { let config: ClientConfig = { ...emptyClientConfig(), toolsConfigured: true, enabledAdapterIds: ["codex"], adapters: [{ adapterId: "codex", packageName: "@atape/adapter-codex", displayName: "Codex", upgradeSpec: "file:/old/codex", version: "0.2.0", installedAt: "before", updatedAt: "before" }] } - let offline = false, installFailed = false + let offline = false, installFailed = false, entryUnavailable = false, entryChecks = 0 const installs: string[] = [], checks: Array<{ name: string; cached: boolean }> = [] const layer = Layer.mergeAll( Layer.succeed(CLISetupPlatform, CLISetupPlatform.of({ runtimeReleaseVersion, @@ -35,8 +37,11 @@ const fixture = (runtimeReleaseVersion = "0.4.4", commandEntryVersion = "0.4.4") ) })), Layer.succeed(CLIUpgradePlatform, CLIUpgradePlatform.of({ acquireOwnership: () => Effect.die("No CLI update ownership during tool inspection"), latest: cached => Effect.suspend(() => { checks.push({ name: "cli", cached }) - return offline ? Effect.fail(new CLIUpgradeError({ reason: "check", message: "offline" })) : Effect.succeed(bundle("0.4.4")) - }), installedVersion: () => Effect.succeed(commandEntryVersion), + return offline ? Effect.fail(new CLIUpgradeError({ reason: "check", message: "offline" })) : Effect.succeed(selected) + }), installedVersion: () => Effect.suspend(() => { + entryChecks++ + return entryUnavailable ? Effect.fail(new CLIUpgradeError({ reason: "installation", message: "Command entry is unavailable" })) : Effect.succeed(commandEntryVersion) + }), install: () => Effect.die("No CLI installation during tool inspection") })), Layer.succeed(AdapterPackages, AdapterPackages.of({ prune: () => Effect.die("Unexpected package maintenance"), install: spec => Effect.sleep(1).pipe(Effect.andThen(Effect.suspend(() => { installs.push(spec) @@ -49,7 +54,8 @@ const fixture = (runtimeReleaseVersion = "0.4.4", commandEntryVersion = "0.4.4") run: (effect: Effect.Effect>, signal?: AbortSignal) => Effect.runPromise(effect.pipe(Effect.provide(layer)), signal ? { signal } : undefined), config: () => structuredClone(config), edit: (value: ClientConfig) => { config = value }, installs, checks, - offline: () => { offline = true }, failInstall: () => { installFailed = true } + offline: () => { offline = true }, failInstall: () => { installFailed = true }, + failEntryInspection: () => { entryUnavailable = true }, entryChecks: () => entryChecks } } @@ -76,10 +82,41 @@ describe("tool releases through the application Interface", () => { } expect(client.installs).toEqual([]) }) - it("keeps command-entry refresh available after the managed runtime has already advanced", async () => { - const rows = await fixture("0.4.4", "0.4.3").run(inspectToolUpdates("0.4.4")) + it.each(["atape.release-bundle.v1", "atape.release-bundle.v2"] as const)("keeps %s command-entry refresh available after the managed runtime has already advanced", async protocol => { + const selected: ReleaseBundle = protocol === "atape.release-bundle.v1" ? bundle("0.4.4") : { + ...bundle("0.4.4"), protocol, migration: { protocol: "atape.capture-migration.v1", id: "journal-v7-to-v8", + fromCaptureStateContracts: ["atape.client.v3-capture.v1", "atape.client.v3-capture.v2"] } + } + const rows = await fixture("0.4.4", "0.4.3", selected).run(inspectToolUpdates("0.4.4")) expect(rows[0]).toMatchObject({ version: "0.4.4", latest: "0.4.4", status: "available", commandEntryVersion: "0.4.3" }) }) + + it("reports the current target-owned release without inspecting or updating its retained older command entry", async () => { + const client = fixture("0.5.12", "0.5.11", targetBundle("0.5.12")), config = client.config() + client.edit({ ...config, adapters: [{ ...config.adapters[0]!, upgradeSpec: "@atape/adapter-codex", version: "0.5.12" }] }) + client.failEntryInspection() + const before = client.config(), row = (await client.run(inspectToolUpdates("0.5.12")))[0]! + expect(row).toMatchObject({ version: "0.5.12", latest: "0.5.12", status: "current" }) + expect(row).not.toHaveProperty("commandEntryVersion"); expect(row).not.toHaveProperty("officialAdapterUpdates") + expect(client.entryChecks()).toBe(0); expect(client.config()).toEqual(before); expect(client.installs).toEqual([]) + }) + + it("keeps complete target-owned bundle alignment available for older official Adapters", async () => { + const client = fixture("0.5.12", "0.5.11", targetBundle("0.5.12")), config = client.config() + client.edit({ ...config, adapters: [{ ...config.adapters[0]!, upgradeSpec: "@atape/adapter-codex", version: "0.5.11" }] }) + const before = client.config(), row = (await client.run(inspectToolUpdates("0.5.12")))[0]! + expect(row).toMatchObject({ version: "0.5.12", latest: "0.5.12", status: "available", officialAdapterUpdates: 1 }) + expect(row).not.toHaveProperty("commandEntryVersion") + expect(client.entryChecks()).toBe(0); expect(client.config()).toEqual(before); expect(client.installs).toEqual([]) + }) + + it("offers a newer target-owned bundle independently of the retained global version", async () => { + const client = fixture("0.5.12", "0.5.11", targetBundle("0.5.13")) + const row = (await client.run(inspectToolUpdates("0.5.12", true)))[0]! + expect(row).toMatchObject({ version: "0.5.12", latest: "0.5.13", status: "available" }) + expect(row).not.toHaveProperty("commandEntryVersion") + expect(client.entryChecks()).toBe(0); expect(client.installs).toEqual([]) + }) it("limits standalone official actions to the actual runtime even when the CLI bundle is newer", async () => { const client = fixture("0.4.3") const rows = await client.run(inspectToolUpdates("0.4.3")) diff --git a/packages/application/src/toolUpdates.ts b/packages/application/src/toolUpdates.ts index 0c28b80..a874ff7 100644 --- a/packages/application/src/toolUpdates.ts +++ b/packages/application/src/toolUpdates.ts @@ -37,7 +37,7 @@ export const inspectToolUpdates = Effect.fn("ToolUpdates.inspect")(function*(cur Effect.flatMap(bundle => { if (!stableVersion(bundle.version)) return Effect.fail(new ToolUpdateError({ message: "Invalid release metadata." })) const row = { ...cliRow, latest: bundle.version, status: releaseStatus(current, bundle.version) } - return row.status === "current" ? cli.installedVersion().pipe(Effect.map(entry => stableVersion(entry) && newer(bundle.version, entry) + return row.status === "current" && bundle.protocol !== "atape.release-bundle.v3" ? cli.installedVersion().pipe(Effect.map(entry => stableVersion(entry) && newer(bundle.version, entry) ? { ...row, status: "available" as const, commandEntryVersion: entry } : row), Effect.mapError(() => new ToolUpdateError({ message: "Command entry metadata is unavailable." }))) : Effect.succeed(row) }), diff --git a/server/internal/releaseinfo/release.go b/server/internal/releaseinfo/release.go index cb36396..4b690ff 100644 --- a/server/internal/releaseinfo/release.go +++ b/server/internal/releaseinfo/release.go @@ -4,9 +4,9 @@ package releaseinfo var ( - Version = "0.5.11" + Version = "0.5.12" AuthEpoch = "auth-v1" - MinimumCLIVersion = "0.5.11" + MinimumCLIVersion = "0.5.12" ) // Info is the small, presentation-safe view exposed by diagnostics and diff --git a/specs/auth-v1-release-gates.json b/specs/auth-v1-release-gates.json index cef3c7f..e18c5ac 100644 --- a/specs/auth-v1-release-gates.json +++ b/specs/auth-v1-release-gates.json @@ -1,8 +1,8 @@ { "protocol": "atape.auth-release-gates.v1", - "releaseVersion": "0.5.11", + "releaseVersion": "0.5.12", "authEpoch": "auth-v1", - "attestation": "docs/releases/evidence/v0.5.11-staging.json", + "attestation": "docs/releases/evidence/v0.5.12-staging.json", "gates": [ { "id": 1, @@ -112,7 +112,7 @@ "evidence": [ {"path": "docs/operations/auth-v1-release-checklist.md", "contains": "TLS / callback / WAF"}, {"path": "docs/operations/backup-and-restore.md", "contains": "restore"}, - {"path": "docs/releases/evidence/v0.5.11-staging.json", "contains": "official-topology"} + {"path": "docs/releases/evidence/v0.5.12-staging.json", "contains": "official-topology"} ], "manualRequirements": ["official-topology", "waf-tls-callback", "backup-restore", "observability-secret-sample", "rollback"] }, @@ -125,8 +125,8 @@ {"path": "specs/auth-v1-release.json", "contains": "atape.auth-release.v1"}, {"path": "scripts/build-release-images.mjs", "contains": "ATAPE_MINIMUM_CLI_VERSION"}, {"path": "scripts/release-contract.mjs", "contains": "MinimumCLIVersion"}, - {"path": "docs/releases/v0.5.11.md", "contains": "180 days"}, - {"path": "docs/releases/evidence/v0.5.11-staging.json", "contains": "two-user-two-team"} + {"path": "docs/releases/v0.5.12.md", "contains": "180 days"}, + {"path": "docs/releases/evidence/v0.5.12-staging.json", "contains": "two-user-two-team"} ], "manualRequirements": ["candidate-artifacts", "two-user-two-team", "release-signoff"] } diff --git a/specs/auth-v1-release.json b/specs/auth-v1-release.json index 6b15ce3..7ef989a 100644 --- a/specs/auth-v1-release.json +++ b/specs/auth-v1-release.json @@ -1,7 +1,7 @@ { "protocol": "atape.auth-release.v1", - "releaseVersion": "0.5.11", + "releaseVersion": "0.5.12", "authEpoch": "auth-v1", - "minimumCliVersion": "0.5.11", - "releaseChannel": "legacy-bridge" + "minimumCliVersion": "0.5.12", + "releaseChannel": "target-owned" } From 6cb25437dc0e6ab689ffc7836458e7e466d366ff Mon Sep 17 00:00:00 2001 From: liying <422107224@qq.com> Date: Tue, 13 Oct 2026 04:42:17 +0800 Subject: [PATCH 2/2] Record authorized manual staging waiver for v0.5.12 candidate --- .../evidence/v0.5.12-manual-waiver.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/releases/evidence/v0.5.12-manual-waiver.json diff --git a/docs/releases/evidence/v0.5.12-manual-waiver.json b/docs/releases/evidence/v0.5.12-manual-waiver.json new file mode 100644 index 0000000..5b01147 --- /dev/null +++ b/docs/releases/evidence/v0.5.12-manual-waiver.json @@ -0,0 +1,23 @@ +{ + "protocol": "atape.manual-release-waiver.v1", + "releaseVersion": "0.5.12", + "authEpoch": "auth-v1", + "status": "authorized", + "candidateCommit": "367e44810519ddc77480e988f7db7a9a631702b5", + "authorizedOn": "2026-10-12", + "authorization": "On 2026-10-12 Asia/Singapore, the user explicitly requested “可以,那你继续发布新版 npm 包,再升级 Server,把文件活动分析上线。” This authorizes coordinated npm publication of the CLI and all seven official Adapters at 0.5.12 after the genuine 0.5.11 automatic-update bridge. The subsequent concern about update refusal is addressed by target-owned preparation, migration and recovery, preserved legacy discovery and the corrected v3 manual global-entry policy. Under ADR-0048, only the listed incomplete manual staging checks are waived. Required exact-head CI, full integration, installed compatibility and security gates must pass. The candidate acceptance record retains earlier failed local attempts and their repairs; no automated failure is waived. Server deployment is separately authorized by the same request, remains a separate action and must complete before this v3-only feature release is advertised. No unrelated production data or schema changes are authorized.", + "scope": "manual-staging-only", + "unverifiedChecks": [ + "candidate-artifacts", + "official-topology", + "github-provider-staging-smoke", + "official-domain-egress-deny-smoke", + "two-user-two-team", + "waf-tls-callback", + "backup-restore", + "observability-secret-sample", + "rollback", + "release-signoff" + ], + "riskNotice": "docs/releases/v0.5.12.md" +}