From 07d079a85624bc1cdfc5b37f659e4c1faa9547b2 Mon Sep 17 00:00:00 2001 From: vitry Date: Thu, 13 Aug 2026 00:46:01 +0800 Subject: [PATCH 1/4] fix: allow strict empty-session auth probe --- scripts/lib/codex-config.mjs | 15 ++++++--- scripts/lib/zcode-client.mjs | 14 ++++++-- scripts/lib/zcode-schema.mjs | 26 +++++++++++++-- tests/fixtures/fake-zcode-cli.mjs | 2 +- tests/setup.test.mjs | 55 ++++++++++++++++++++++++++++++- 5 files changed, 101 insertions(+), 11 deletions(-) diff --git a/scripts/lib/codex-config.mjs b/scripts/lib/codex-config.mjs index 4ba01694..ec655e97 100644 --- a/scripts/lib/codex-config.mjs +++ b/scripts/lib/codex-config.mjs @@ -34,7 +34,7 @@ export async function runSetup(input) { catch (error) { if (error?.code === 'ZCODE_NOT_FOUND' || error?.code === 'ZCODE_VERSION_UNSUPPORTED') return reportAndPersist(input, { path: null, version: null }, { ready: false }, error.code === 'ZCODE_NOT_FOUND' ? 'missing' : 'outdated', error.message, false); throw error; } const hooks = await client.request('hooks/list', { cwds: [cwd] }); const inspected = await validateHooks(hooks, cwd, pluginRoot, hooksPath); if (!inspected.ok) return reportAndPersist(input, discovery, { ready: false }, 'untrusted', inspected.reason, false); - const auth = await diagnoseZCodeAuth({ workspace: cwd, discovery, env: input.env }); if (!auth.ready) return reportAndPersist(input, discovery, auth, 'unauthenticated', auth.reason, false); + const auth = await diagnoseZCodeAuth({ workspace: cwd, discovery, env: input.env }); if (!auth.ready) return reportAndPersist(input, discovery, auth, auth.status === 'incompatible' ? 'incompatible' : 'unauthenticated', auth.reason, false); const edits = []; if (config?.config?.features?.hooks !== true) edits.push({ keyPath: 'features.hooks', value: true, mergeStrategy: 'upsert' }); const trust = {}; for (const hook of inspected.hooks) if (!['trusted', 'managed'].includes(hook.trustStatus)) trust[hook.key] = { trusted_hash: hook.currentHash }; if (Object.keys(trust).length) edits.push({ keyPath: 'hooks.state', value: trust, mergeStrategy: 'upsert' }); const packageJson = JSON.parse(await readFile(join(pluginRoot, 'package.json'), 'utf8')); const template = await readFile(join(pluginRoot, 'agents', 'zcode-rescue.toml.template'), 'utf8'); @@ -159,16 +159,23 @@ export async function diagnoseZCodeAuth(input) { let client; let sessionId; try { client = await (input.createClient ?? createZCodeClient)({ workspace: input.workspace, launch: input.discovery.launch, env: input.env, requestTimeoutMs: input.requestTimeoutMs ?? 2_000 }); - const snapshot = await client.createSession({ workspace: input.workspace }); + const create = client.createSessionForSetupAuthProbe ?? client.createSession; + const snapshot = await create.call(client, { workspace: input.workspace }); sessionId = snapshot.session.sessionId; return { ready: true, status: 'authenticated' }; } catch (error) { if (error?.code === 'ZCODE_REQUEST_FAILED' && error.details?.method === 'session/create' && error.details.remoteCode === 'model_config_missing') { return { ready: false, status: 'unauthenticated', reason: 'ZCode CLI model provider is not configured.', remedy: 'Configure an API-key provider in ZCode CLI, then run $zcode:setup again.' }; } - return { ready: false, status: 'unauthenticated', reason: 'ZCode session/create could not prove model authentication.', remedy: 'Authenticate with ZCode, then run $zcode:setup again.' }; + if (error?.code === 'ZCODE_REQUEST_FAILED') { + return { ready: false, status: 'unauthenticated', reason: 'ZCode session/create could not prove model authentication.', remedy: 'Authenticate with ZCode, then run $zcode:setup again.' }; + } + if (error?.code === 'ZCODE_OUTPUT_INVALID' || error?.category === 'protocol') { + return { ready: false, status: 'incompatible', reason: 'ZCode session/create returned a snapshot incompatible with the setup probe contract.', remedy: 'Upgrade ZCode to a compatible protocol version, then run $zcode:setup again.' }; + } + return { ready: false, status: 'incompatible', reason: 'ZCode session/create could not be verified by the setup authentication probe.', remedy: 'Check the ZCode CLI protocol and rerun $zcode:setup.' }; } finally { - if (sessionId) await client?.stopSession(sessionId).catch(() => {}); + if (sessionId && typeof client?.stopSession === 'function') await client.stopSession(sessionId).catch(() => {}); await client?.close().catch(() => {}); } } diff --git a/scripts/lib/zcode-client.mjs b/scripts/lib/zcode-client.mjs index 1bb10c8b..83bdbd0f 100644 --- a/scripts/lib/zcode-client.mjs +++ b/scripts/lib/zcode-client.mjs @@ -5,7 +5,7 @@ import { readdir, realpath } from 'node:fs/promises'; import { PluginError } from './errors.mjs'; import { isBoundedPublicIdentifier, isSafeIdentifier } from './identifier.mjs'; import { closeProtocolUntil, connectZCodeBroker, MAX_DRAIN_TIMEOUT_MS, spawnZCodeProtocol } from './zcode-protocol.mjs'; -import { validSessionInfo, validSnapshot as snapshotValid } from './zcode-schema.mjs'; +import { validSessionInfo, validSetupAuthProbeSnapshot, validSnapshot as snapshotValid } from './zcode-schema.mjs'; import { brokerEndpointFor, brokerIdentityNameForWireOptions, ensureZCodeBroker, inspectBrokerIdentity, MAX_BROKER_IDLE_TIMEOUT_MS, MIN_BROKER_IDLE_TIMEOUT_MS, prioritizeBrokerOwnership } from '../zcode-broker.mjs'; import { resolveWorkspaceStorage } from './workspace.mjs'; @@ -22,6 +22,16 @@ export class ZCodeClient { /** @param {{workspace:string,sessionId?:string,model?:{providerId:string,modelId:string,variant?:string},importedHistory?:{title?:string,createdAt?:number,updatedAt?:number,messages:Array<{role:'user'|'assistant',content:string,timestamp?:number}>}}} input */ async createSession(input) { + return this.createSessionValidated(input, snapshotValid); + } + + /** @param {{workspace:string,sessionId?:string,model?:{providerId:string,modelId:string,variant?:string},importedHistory?:{title?:string,createdAt?:number,updatedAt?:number,messages:Array<{role:'user'|'assistant',content:string,timestamp?:number}>}}} input Setup-only compatibility probe; formal runtime callers must use createSession(). */ + async createSessionForSetupAuthProbe(input) { + return this.createSessionValidated(input, validSetupAuthProbeSnapshot); + } + + /** @param {{workspace:string,sessionId?:string,model?:{providerId:string,modelId:string,variant?:string},importedHistory?:{title?:string,createdAt?:number,updatedAt?:number,messages:Array<{role:'user'|'assistant',content:string,timestamp?:number}>}}} input @param {(value:any,sessionId:string,workspace:string)=>boolean} validator */ + async createSessionValidated(input, validator) { requireExactObject(input, ['workspace'], ['sessionId', 'model', 'importedHistory']); requireString(input.workspace); if (input.sessionId !== undefined) requireSessionId(input.sessionId); @@ -39,7 +49,7 @@ export class ZCodeClient { if (input.importedHistory !== undefined) params.importedHistory = normalizeImportedHistory(input.importedHistory); const result = await this.protocol.request('session/create', params); if (!plainObject(result) || !plainObject(result.session) || !isSafeIdentifier(result.session.sessionId) || input.sessionId && result.session.sessionId !== input.sessionId) throw outputError('session/create'); - validateSnapshot(result, result.session.sessionId, workspacePath, 'session/create'); + if (!validator(result, result.session.sessionId, workspacePath)) throw outputError('session/create'); this.sessionWorkspaces.set(result.session.sessionId, workspacePath); if (plainObject(result.settings?.model) && Array.isArray(result.settings.model.available)) this.sessionCatalogs.set(result.session.sessionId, result.settings.model); return result; diff --git a/scripts/lib/zcode-schema.mjs b/scripts/lib/zcode-schema.mjs index db2fcb26..0c940fb2 100644 --- a/scripts/lib/zcode-schema.mjs +++ b/scripts/lib/zcode-schema.mjs @@ -149,11 +149,31 @@ function validSnapshotRelations(value, sessionId, workspacePath) { && value.messages.every((/** @type {any} */ message) => message.info.sessionId === sessionId && message.parts.every((/** @type {any} */ part) => part.sessionId === sessionId && part.messageId === message.info.messageId)); } -/** Kne */ -export function validSnapshot(/** @type {any} */ value, /** @type {string} */ sessionId, /** @type {string} */ workspacePath) { +/** Validate a complete runtime snapshot with strict session identity relations. */ +function validSnapshotEnvelope(/** @type {any} */ value) { return exact(value, ['protocol', 'session', 'settings', 'projection', 'runtime', 'messages'], ['goalStats', 'todos', 'todoGroups', 'slashCommands']) && exact(value.protocol, ['name', 'version']) && value.protocol.name === 'ZCode Protocol' && value.protocol.version === 1 && validSessionInfo(value.session) && validSettings(value.settings) && validProjection(value.projection) && validRuntime(value.runtime) && arrayOf(value.messages, validMessage) - && validSnapshotRelations(value, sessionId, workspacePath) && optional(value.goalStats, validGoalStats) && optional(value.todos, (items) => arrayOf(items, validTodo)) && optional(value.todoGroups, (items) => arrayOf(items, validTodoGroup)) && optional(value.slashCommands, (items) => arrayOf(items, validSlashCommand)); } + +export function validSnapshot(/** @type {any} */ value, /** @type {string} */ sessionId, /** @type {string} */ workspacePath) { + return validSnapshotEnvelope(value) && validSnapshotRelations(value, sessionId, workspacePath); +} + +/** + * Setup-only compatibility validation for ZCode 0.16.1's empty projection. + * The normal validSnapshot relation remains strict for all runtime paths. + * @param {any} value @param {string} sessionId @param {string} workspacePath + */ +export function validSetupAuthProbeSnapshot(value, sessionId, workspacePath) { + return validSnapshotEnvelope(value) + && text(sessionId) && value.session.sessionId === sessionId + && value.session.workspace.workspacePath === workspacePath && value.session.workspace.workspaceKey === workspacePath + && value.session.status === 'idle' + && value.projection.sessionId === 'unknown' && value.projection.status === 'idle' + && (value.session.target === undefined || value.session.target === null) + && (value.projection.target === undefined || value.projection.target === null) + && value.projection.pendingPermissions.length === 0 && value.projection.activeToolCalls.length === 0 && value.projection.backgroundJobs.length === 0 + && value.runtime.eventSeq === 0 && value.runtime.pendingRequestIds.length === 0 && value.messages.length === 0; +} diff --git a/tests/fixtures/fake-zcode-cli.mjs b/tests/fixtures/fake-zcode-cli.mjs index 69d947fa..4d4d6fff 100644 --- a/tests/fixtures/fake-zcode-cli.mjs +++ b/tests/fixtures/fake-zcode-cli.mjs @@ -43,7 +43,7 @@ function resultMessages(sessionId, model, review, suffix = 'current', selectedMo : [{ ...base, type: 'text', text: gateText === '__EMPTY__' ? '' : gateText ?? (review ? JSON.stringify({ findings: [] }) : resultText ?? 'done') }]; return [{ info: { messageId: userId, sessionId, role: 'user', time: { created: 1, completed: 2 }, agent: 'build', model, synthetic: false, visibility: 'user-visible' }, parts: [{ partId: `part-user-${suffix}`, sessionId, messageId: userId, type: 'text', text: 'hello' }] }, { info: { messageId: assistantId, sessionId, role: 'assistant', time: { created: 2, completed: 3 }, parentMessageId: userId, agent: 'build', model, path: { cwd: '/repo', root: '/repo' }, cost: 0, tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } }, finish: 'stop', ...(structured === undefined ? {} : { structured }) }, parts }]; } -function snapshot(sessionId, value = sessions.get(sessionId)) { const valueSettings = value?.settings ?? settings(); const status = value?.projectionStatus ?? 'idle'; return { protocol: { name: 'ZCode Protocol', version: 1 }, session: { ...sessionInfo(sessionId, value?.workspacePath ?? process.env.FAKE_ZCODE_WORKSPACE ?? process.cwd(), status), model: valueSettings.model.current }, settings: valueSettings, projection: { sessionId, status, mode: 'build', turnCount: 0, totalTokenCount: 0, contextUsed: 0, contextWindow: 128000, pendingPermissions: [], activeToolCalls: [], backgroundJobs: [] }, runtime: { eventSeq: 0, stateRevision: value?.stateRevision ?? 0, pendingRequestIds: [] }, messages: value?.messages?.length ? value.messages : value?.pendingResult ? [] : messages(sessionId, valueSettings.model.current), goalStats: { timeUsedSeconds: 0, tokensUsed: 0, tokenBudget: null, contextUsed: 0, contextWindow: 128000, toolCallCount: 0, iterationCount: 0 }, todos: [{ content: 'Verify', status: 'pending', priority: 'high' }], todoGroups: [{ id: 'todo-group-1', source: 'session', todos: [] }], slashCommands: [{ name: 'review', description: 'Review code', source: 'builtin' }] }; } +function snapshot(sessionId, value = sessions.get(sessionId)) { const valueSettings = value?.settings ?? settings(); const status = value?.projectionStatus ?? 'idle'; const empty = process.env.FAKE_ZCODE_EMPTY_SESSION === '1'; const emptyVariant = process.env.FAKE_ZCODE_EMPTY_SESSION_VARIANT; const projectionSessionId = empty ? emptyVariant === 'conflict' ? 'other-session' : 'unknown' : sessionId; const projectionStatus = empty && emptyVariant === 'non-idle' ? 'running' : status; const runtimeEventSeq = empty && emptyVariant === 'event-seq' ? 1 : 0; const emptyMessages = empty && emptyVariant !== 'messages'; return { protocol: { name: 'ZCode Protocol', version: 1 }, session: { ...sessionInfo(sessionId, value?.workspacePath ?? process.env.FAKE_ZCODE_WORKSPACE ?? process.cwd(), status), model: valueSettings.model.current }, settings: valueSettings, projection: { sessionId: projectionSessionId, status: projectionStatus, mode: 'build', turnCount: 0, totalTokenCount: 0, contextUsed: 0, contextWindow: 128000, pendingPermissions: [], activeToolCalls: [], backgroundJobs: [], ...(empty && emptyVariant === 'target' ? { target: { sessionId: 'other-session', targetId: 'target-1', objective: 'foreign', summaryTitle: null, status: 'active', tokenBudget: null, tokensUsed: 0, timeUsedSeconds: 0, createdAt: 1, updatedAt: 1 } } : {}) }, runtime: { eventSeq: runtimeEventSeq, stateRevision: value?.stateRevision ?? 0, pendingRequestIds: [] }, messages: emptyMessages ? [] : value?.messages?.length ? value.messages : value?.pendingResult ? [] : messages(sessionId, valueSettings.model.current), goalStats: { timeUsedSeconds: 0, tokensUsed: 0, tokenBudget: null, contextUsed: 0, contextWindow: 128000, toolCallCount: 0, iterationCount: 0 }, todos: [{ content: 'Verify', status: 'pending', priority: 'high' }], todoGroups: [{ id: 'todo-group-1', source: 'session', todos: [] }], slashCommands: [{ name: 'review', description: 'Review code', source: 'builtin' }] }; } function corruptSnapshot(result, variant) { if (variant === 'missing-workspace') delete result.session.workspace; diff --git a/tests/setup.test.mjs b/tests/setup.test.mjs index 43bd8953..6edf8725 100644 --- a/tests/setup.test.mjs +++ b/tests/setup.test.mjs @@ -37,7 +37,7 @@ async function context({ hooks = hookMetadata(root), features = { hooks: false } const writable = { sandbox_workspace_write: { writable_roots: [dataRoot] } }; const configResult = { config: { features, unrelated: { preserved: true }, ...writable }, origins: {}, layers: [{ name: { type: 'user', file: join(dataRoot, 'config.toml') }, version: 'version-1', config: { unrelated: { preserved: true }, ...writable } }] }; const hooksResult = { data: [{ cwd, errors: [], warnings: [], hooks }] }; - return { cwd, dataRoot, record, zcodeRecord, options: { pluginRoot: root, dataRoot, cwd, reviewGate: undefined, sessionStartedAt: '2000-01-01T00:00:00.000Z', env: { ...process.env, ZCODE_PATH: fakeZCode, FAKE_ZCODE_RECORD: zcodeRecord, FAKE_CODEX_RECORD: record, FAKE_CODEX_CONFIG_RESULT: JSON.stringify(configResult), FAKE_CODEX_HOOKS_RESULT: JSON.stringify(hooksResult), ...zcodeEnv, ...codexEnv }, codex: { executable: process.execPath, args: [fakeCodex], timeoutMs: 5_000 } } }; + return { cwd, dataRoot, record, zcodeRecord, options: { pluginRoot: root, dataRoot, cwd, reviewGate: undefined, sessionStartedAt: '2000-01-01T00:00:00.000Z', env: { ...process.env, ZCODE_PATH: fakeZCode, FAKE_ZCODE_EMPTY_SESSION: '1', FAKE_ZCODE_RECORD: zcodeRecord, FAKE_CODEX_RECORD: record, FAKE_CODEX_CONFIG_RESULT: JSON.stringify(configResult), FAKE_CODEX_HOOKS_RESULT: JSON.stringify(hooksResult), ...zcodeEnv, ...codexEnv }, codex: { executable: process.execPath, args: [fakeCodex], timeoutMs: 5_000 } } }; } async function recordSetupSession(ctx, sessionId, prompt) { @@ -228,6 +228,59 @@ test('plugin-level authentication diagnostic is session/create based and actiona assert.match(unavailable.remedy, /authenticate.*ZCode/i); }); +test('setup accepts ZCode 0.16.1 empty-session projection unknown', async () => { + const ready = await context({ zcodeEnv: { FAKE_ZCODE_EMPTY_SESSION: '1' } }); + const diagnostic = await diagnoseZCodeAuth({ workspace: ready.cwd, discovery: { launch: { command: process.execPath, args: [fakeZCode], target: fakeZCode } }, env: ready.options.env }); + assert.deepEqual(diagnostic, { ready: true, status: 'authenticated' }); +}); + +test('setup rejects non-empty or conflicting empty-session snapshots', async () => { + for (const variant of ['conflict', 'messages', 'target', 'non-idle', 'event-seq']) { + const ready = await context({ zcodeEnv: { FAKE_ZCODE_EMPTY_SESSION: '1', FAKE_ZCODE_EMPTY_SESSION_VARIANT: variant } }); + const diagnostic = await diagnoseZCodeAuth({ workspace: ready.cwd, discovery: { launch: { command: process.execPath, args: [fakeZCode], target: fakeZCode } }, env: ready.options.env }); + assert.equal(diagnostic.ready, false, variant); + assert.equal(diagnostic.status, 'incompatible', variant); + } +}); + +test('setup reports an incompatible empty-session protocol instead of unauthenticated', async () => { + const ctx = await context({ zcodeEnv: { FAKE_ZCODE_EMPTY_SESSION: '1', FAKE_ZCODE_EMPTY_SESSION_VARIANT: 'conflict' } }); + const report = await runSetup(ctx.options); + assert.equal(report.status, 'incompatible'); + assert.equal(report.auth.status, 'incompatible'); + assert.match(report.auth.reason, /incompatible|protocol|snapshot/i); +}); + +test('setup authentication uses the dedicated empty-session probe seam', async () => { + let probeCalls = 0; + const diagnostic = await diagnoseZCodeAuth({ + workspace: '/repo', + discovery: { launch: { command: 'unused', args: [] } }, + createClient: async () => ({ + createSession: async () => { throw new Error('setup must not use the strict runtime createSession seam'); }, + createSessionForSetupAuthProbe: async ({ workspace }) => { probeCalls += 1; assert.equal(workspace, '/repo'); return { session: { sessionId: 'setup-session' } }; }, + stopSession: async () => {}, close: async () => {}, + }), + }); + assert.equal(probeCalls, 1); + assert.deepEqual(diagnostic, { ready: true, status: 'authenticated' }); +}); + +test('setup does not mislabel protocol output invalid as unauthenticated', async () => { + const diagnostic = await diagnoseZCodeAuth({ + workspace: '/repo', + discovery: { launch: { command: 'unused', args: [] } }, + createClient: async () => ({ + createSessionForSetupAuthProbe: async () => { throw Object.assign(new Error('invalid snapshot'), { code: 'ZCODE_OUTPUT_INVALID', details: { method: 'session/create' } }); }, + close: async () => {}, + }), + }); + assert.equal(diagnostic.ready, false); + assert.equal(diagnostic.status, 'incompatible'); + assert.match(diagnostic.reason, /protocol|snapshot|incompatible/i); + assert.doesNotMatch(diagnostic.reason, /unauthenticated/i); +}); + test('plugin-level authentication diagnostic identifies a missing ZCode CLI model provider', async () => { const ready = await context({ hooks: hookMetadata(root, 'trusted'), features: { hooks: true } }); const discovery = { launch: { command: process.execPath, args: [fakeZCode], target: fakeZCode } }; From b3cd8ecd8d7dbc51b61a789b44265e0a2ab5b865 Mon Sep 17 00:00:00 2001 From: vitry Date: Thu, 13 Aug 2026 00:46:52 +0800 Subject: [PATCH 2/4] build: refresh setup auth probe snapshot --- marketplace/.agents/plugins/provenance.json | 4 +-- .../zcode/scripts/lib/codex-config.mjs | 15 ++++++++--- .../zcode/scripts/lib/zcode-client.mjs | 14 ++++++++-- .../zcode/scripts/lib/zcode-schema.mjs | 26 ++++++++++++++++--- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/marketplace/.agents/plugins/provenance.json b/marketplace/.agents/plugins/provenance.json index 8550d7a8..cdfa9e27 100644 --- a/marketplace/.agents/plugins/provenance.json +++ b/marketplace/.agents/plugins/provenance.json @@ -1,8 +1,8 @@ { "packageVersion": "0.1.0", "pluginVersion": "0.1.0", - "sourceRef": "15785039498ab0c63d36890a7783cfe6a8c101e7", - "sourceSha": "15785039498ab0c63d36890a7783cfe6a8c101e7", + "sourceRef": "main", + "sourceSha": "07d079a85624bc1cdfc5b37f659e4c1faa9547b2", "dependencyLock": { "file": "npm-shrinkwrap.json", "sha256": "fa927194e6ca0b25c1d3f428859b2ab4798b8eabb4d31bc87188d73c630f9938" diff --git a/marketplace/plugins/zcode/scripts/lib/codex-config.mjs b/marketplace/plugins/zcode/scripts/lib/codex-config.mjs index 4ba01694..ec655e97 100644 --- a/marketplace/plugins/zcode/scripts/lib/codex-config.mjs +++ b/marketplace/plugins/zcode/scripts/lib/codex-config.mjs @@ -34,7 +34,7 @@ export async function runSetup(input) { catch (error) { if (error?.code === 'ZCODE_NOT_FOUND' || error?.code === 'ZCODE_VERSION_UNSUPPORTED') return reportAndPersist(input, { path: null, version: null }, { ready: false }, error.code === 'ZCODE_NOT_FOUND' ? 'missing' : 'outdated', error.message, false); throw error; } const hooks = await client.request('hooks/list', { cwds: [cwd] }); const inspected = await validateHooks(hooks, cwd, pluginRoot, hooksPath); if (!inspected.ok) return reportAndPersist(input, discovery, { ready: false }, 'untrusted', inspected.reason, false); - const auth = await diagnoseZCodeAuth({ workspace: cwd, discovery, env: input.env }); if (!auth.ready) return reportAndPersist(input, discovery, auth, 'unauthenticated', auth.reason, false); + const auth = await diagnoseZCodeAuth({ workspace: cwd, discovery, env: input.env }); if (!auth.ready) return reportAndPersist(input, discovery, auth, auth.status === 'incompatible' ? 'incompatible' : 'unauthenticated', auth.reason, false); const edits = []; if (config?.config?.features?.hooks !== true) edits.push({ keyPath: 'features.hooks', value: true, mergeStrategy: 'upsert' }); const trust = {}; for (const hook of inspected.hooks) if (!['trusted', 'managed'].includes(hook.trustStatus)) trust[hook.key] = { trusted_hash: hook.currentHash }; if (Object.keys(trust).length) edits.push({ keyPath: 'hooks.state', value: trust, mergeStrategy: 'upsert' }); const packageJson = JSON.parse(await readFile(join(pluginRoot, 'package.json'), 'utf8')); const template = await readFile(join(pluginRoot, 'agents', 'zcode-rescue.toml.template'), 'utf8'); @@ -159,16 +159,23 @@ export async function diagnoseZCodeAuth(input) { let client; let sessionId; try { client = await (input.createClient ?? createZCodeClient)({ workspace: input.workspace, launch: input.discovery.launch, env: input.env, requestTimeoutMs: input.requestTimeoutMs ?? 2_000 }); - const snapshot = await client.createSession({ workspace: input.workspace }); + const create = client.createSessionForSetupAuthProbe ?? client.createSession; + const snapshot = await create.call(client, { workspace: input.workspace }); sessionId = snapshot.session.sessionId; return { ready: true, status: 'authenticated' }; } catch (error) { if (error?.code === 'ZCODE_REQUEST_FAILED' && error.details?.method === 'session/create' && error.details.remoteCode === 'model_config_missing') { return { ready: false, status: 'unauthenticated', reason: 'ZCode CLI model provider is not configured.', remedy: 'Configure an API-key provider in ZCode CLI, then run $zcode:setup again.' }; } - return { ready: false, status: 'unauthenticated', reason: 'ZCode session/create could not prove model authentication.', remedy: 'Authenticate with ZCode, then run $zcode:setup again.' }; + if (error?.code === 'ZCODE_REQUEST_FAILED') { + return { ready: false, status: 'unauthenticated', reason: 'ZCode session/create could not prove model authentication.', remedy: 'Authenticate with ZCode, then run $zcode:setup again.' }; + } + if (error?.code === 'ZCODE_OUTPUT_INVALID' || error?.category === 'protocol') { + return { ready: false, status: 'incompatible', reason: 'ZCode session/create returned a snapshot incompatible with the setup probe contract.', remedy: 'Upgrade ZCode to a compatible protocol version, then run $zcode:setup again.' }; + } + return { ready: false, status: 'incompatible', reason: 'ZCode session/create could not be verified by the setup authentication probe.', remedy: 'Check the ZCode CLI protocol and rerun $zcode:setup.' }; } finally { - if (sessionId) await client?.stopSession(sessionId).catch(() => {}); + if (sessionId && typeof client?.stopSession === 'function') await client.stopSession(sessionId).catch(() => {}); await client?.close().catch(() => {}); } } diff --git a/marketplace/plugins/zcode/scripts/lib/zcode-client.mjs b/marketplace/plugins/zcode/scripts/lib/zcode-client.mjs index 1bb10c8b..83bdbd0f 100644 --- a/marketplace/plugins/zcode/scripts/lib/zcode-client.mjs +++ b/marketplace/plugins/zcode/scripts/lib/zcode-client.mjs @@ -5,7 +5,7 @@ import { readdir, realpath } from 'node:fs/promises'; import { PluginError } from './errors.mjs'; import { isBoundedPublicIdentifier, isSafeIdentifier } from './identifier.mjs'; import { closeProtocolUntil, connectZCodeBroker, MAX_DRAIN_TIMEOUT_MS, spawnZCodeProtocol } from './zcode-protocol.mjs'; -import { validSessionInfo, validSnapshot as snapshotValid } from './zcode-schema.mjs'; +import { validSessionInfo, validSetupAuthProbeSnapshot, validSnapshot as snapshotValid } from './zcode-schema.mjs'; import { brokerEndpointFor, brokerIdentityNameForWireOptions, ensureZCodeBroker, inspectBrokerIdentity, MAX_BROKER_IDLE_TIMEOUT_MS, MIN_BROKER_IDLE_TIMEOUT_MS, prioritizeBrokerOwnership } from '../zcode-broker.mjs'; import { resolveWorkspaceStorage } from './workspace.mjs'; @@ -22,6 +22,16 @@ export class ZCodeClient { /** @param {{workspace:string,sessionId?:string,model?:{providerId:string,modelId:string,variant?:string},importedHistory?:{title?:string,createdAt?:number,updatedAt?:number,messages:Array<{role:'user'|'assistant',content:string,timestamp?:number}>}}} input */ async createSession(input) { + return this.createSessionValidated(input, snapshotValid); + } + + /** @param {{workspace:string,sessionId?:string,model?:{providerId:string,modelId:string,variant?:string},importedHistory?:{title?:string,createdAt?:number,updatedAt?:number,messages:Array<{role:'user'|'assistant',content:string,timestamp?:number}>}}} input Setup-only compatibility probe; formal runtime callers must use createSession(). */ + async createSessionForSetupAuthProbe(input) { + return this.createSessionValidated(input, validSetupAuthProbeSnapshot); + } + + /** @param {{workspace:string,sessionId?:string,model?:{providerId:string,modelId:string,variant?:string},importedHistory?:{title?:string,createdAt?:number,updatedAt?:number,messages:Array<{role:'user'|'assistant',content:string,timestamp?:number}>}}} input @param {(value:any,sessionId:string,workspace:string)=>boolean} validator */ + async createSessionValidated(input, validator) { requireExactObject(input, ['workspace'], ['sessionId', 'model', 'importedHistory']); requireString(input.workspace); if (input.sessionId !== undefined) requireSessionId(input.sessionId); @@ -39,7 +49,7 @@ export class ZCodeClient { if (input.importedHistory !== undefined) params.importedHistory = normalizeImportedHistory(input.importedHistory); const result = await this.protocol.request('session/create', params); if (!plainObject(result) || !plainObject(result.session) || !isSafeIdentifier(result.session.sessionId) || input.sessionId && result.session.sessionId !== input.sessionId) throw outputError('session/create'); - validateSnapshot(result, result.session.sessionId, workspacePath, 'session/create'); + if (!validator(result, result.session.sessionId, workspacePath)) throw outputError('session/create'); this.sessionWorkspaces.set(result.session.sessionId, workspacePath); if (plainObject(result.settings?.model) && Array.isArray(result.settings.model.available)) this.sessionCatalogs.set(result.session.sessionId, result.settings.model); return result; diff --git a/marketplace/plugins/zcode/scripts/lib/zcode-schema.mjs b/marketplace/plugins/zcode/scripts/lib/zcode-schema.mjs index db2fcb26..0c940fb2 100644 --- a/marketplace/plugins/zcode/scripts/lib/zcode-schema.mjs +++ b/marketplace/plugins/zcode/scripts/lib/zcode-schema.mjs @@ -149,11 +149,31 @@ function validSnapshotRelations(value, sessionId, workspacePath) { && value.messages.every((/** @type {any} */ message) => message.info.sessionId === sessionId && message.parts.every((/** @type {any} */ part) => part.sessionId === sessionId && part.messageId === message.info.messageId)); } -/** Kne */ -export function validSnapshot(/** @type {any} */ value, /** @type {string} */ sessionId, /** @type {string} */ workspacePath) { +/** Validate a complete runtime snapshot with strict session identity relations. */ +function validSnapshotEnvelope(/** @type {any} */ value) { return exact(value, ['protocol', 'session', 'settings', 'projection', 'runtime', 'messages'], ['goalStats', 'todos', 'todoGroups', 'slashCommands']) && exact(value.protocol, ['name', 'version']) && value.protocol.name === 'ZCode Protocol' && value.protocol.version === 1 && validSessionInfo(value.session) && validSettings(value.settings) && validProjection(value.projection) && validRuntime(value.runtime) && arrayOf(value.messages, validMessage) - && validSnapshotRelations(value, sessionId, workspacePath) && optional(value.goalStats, validGoalStats) && optional(value.todos, (items) => arrayOf(items, validTodo)) && optional(value.todoGroups, (items) => arrayOf(items, validTodoGroup)) && optional(value.slashCommands, (items) => arrayOf(items, validSlashCommand)); } + +export function validSnapshot(/** @type {any} */ value, /** @type {string} */ sessionId, /** @type {string} */ workspacePath) { + return validSnapshotEnvelope(value) && validSnapshotRelations(value, sessionId, workspacePath); +} + +/** + * Setup-only compatibility validation for ZCode 0.16.1's empty projection. + * The normal validSnapshot relation remains strict for all runtime paths. + * @param {any} value @param {string} sessionId @param {string} workspacePath + */ +export function validSetupAuthProbeSnapshot(value, sessionId, workspacePath) { + return validSnapshotEnvelope(value) + && text(sessionId) && value.session.sessionId === sessionId + && value.session.workspace.workspacePath === workspacePath && value.session.workspace.workspaceKey === workspacePath + && value.session.status === 'idle' + && value.projection.sessionId === 'unknown' && value.projection.status === 'idle' + && (value.session.target === undefined || value.session.target === null) + && (value.projection.target === undefined || value.projection.target === null) + && value.projection.pendingPermissions.length === 0 && value.projection.activeToolCalls.length === 0 && value.projection.backgroundJobs.length === 0 + && value.runtime.eventSeq === 0 && value.runtime.pendingRequestIds.length === 0 && value.messages.length === 0; +} From 7c9b9dbaa7bb6a63dec82b953066f2cc4ea0fc75 Mon Sep 17 00:00:00 2001 From: vitry Date: Thu, 13 Aug 2026 00:49:10 +0800 Subject: [PATCH 3/4] test: model empty setup probe in marketplace install --- tests/integration/marketplace-install.test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/marketplace-install.test.mjs b/tests/integration/marketplace-install.test.mjs index 62ff0b8d..a728f193 100644 --- a/tests/integration/marketplace-install.test.mjs +++ b/tests/integration/marketplace-install.test.mjs @@ -200,6 +200,7 @@ test('isolated Codex marketplace lists and installs the eight-skill snapshot', a cwd: temporary, env: { ...env, + FAKE_ZCODE_EMPTY_SESSION: '1', CODEX_APP_SERVER_PATH: process.execPath, CODEX_APP_SERVER_ARGS_JSON: JSON.stringify([join(root, 'tests', 'fixtures', 'fake-codex-app-server.mjs')]), FAKE_CODEX_RECORD: setupRecord, @@ -225,6 +226,7 @@ test('isolated Codex marketplace lists and installs the eight-skill snapshot', a cwd: temporary, env: { ...env, + FAKE_ZCODE_EMPTY_SESSION: '1', ZCODE_PATH: join(root, 'tests', 'fixtures', 'fake-zcode-cli.mjs'), CODEX_APP_SERVER_PATH: process.execPath, CODEX_APP_SERVER_ARGS_JSON: JSON.stringify([join(root, 'tests', 'fixtures', 'fake-codex-app-server.mjs')]), From 5cd0e88458dad0906965a5e5826efbd8771e80e2 Mon Sep 17 00:00:00 2001 From: vitry Date: Thu, 13 Aug 2026 00:51:41 +0800 Subject: [PATCH 4/4] test: cover empty probe in installed setup reruns --- tests/integration/marketplace-install.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/marketplace-install.test.mjs b/tests/integration/marketplace-install.test.mjs index a728f193..478bcdde 100644 --- a/tests/integration/marketplace-install.test.mjs +++ b/tests/integration/marketplace-install.test.mjs @@ -262,6 +262,7 @@ test('isolated Codex marketplace lists and installs the eight-skill snapshot', a cwd: temporary, env: { ...env, + FAKE_ZCODE_EMPTY_SESSION: '1', ZCODE_PATH: join(root, 'tests', 'fixtures', 'fake-zcode-cli.mjs'), CODEX_APP_SERVER_PATH: process.execPath, CODEX_APP_SERVER_ARGS_JSON: JSON.stringify([join(root, 'tests', 'fixtures', 'fake-codex-app-server.mjs')]),