cli-aimlock 7.0.37 → 7.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/aimlock-chain-calls.mjs +10 -2
- package/aimlock-chain-executor.mjs +4 -5
- package/aimlock-coordination.mjs +4 -3
- package/aimlock-runtime.mjs +1 -1
- package/aimlock-tasks-cli.mjs +202 -0
- package/brain-client.mjs +13 -1
- package/broker-account-storage.mjs +135 -0
- package/broker-credentials.mjs +125 -0
- package/broker.mjs +13 -93
- package/cli.mjs +4 -1
- package/installer-storage.mjs +205 -0
- package/installer.mjs +48 -63
- package/official-skill-update.mjs +151 -0
- package/package.json +11 -4
- package/skill/SKILL.md +45 -6
- package/skill/references/task-routing.md +13 -0
- package/skill/skill.json +1 -1
package/broker.mjs
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { brainClientAuthorization } from './broker-credentials.mjs'
|
|
3
|
+
import { prepareOfficialSkillUse, withUpgradeMetadata } from './official-skill-update.mjs'
|
|
4
|
+
export { brainClientAuthorization, brainClientTokenPath } from './broker-credentials.mjs'
|
|
5
|
+
export { LOOKUP_TIMEOUT_MS } from './official-skill-update.mjs'
|
|
5
6
|
import { OfficialSkillInvocationError, OfficialSkillResponseError, transportFailureCode, transportDiagnostics } from './broker-failures.mjs'
|
|
6
7
|
import { queryOfficialSkillReceipt, SKILL_RECEIPT_HEADER, SKILL_RECEIPT_SCHEMA } from './broker-recovery.mjs'
|
|
7
8
|
export { officialSkillFailureResponse, transportFailureCode } from './broker-failures.mjs'
|
|
8
9
|
|
|
9
|
-
export const LOOKUP_TIMEOUT_MS = 8000
|
|
10
10
|
export const CALL_TIMEOUT_MS = 120_000
|
|
11
|
-
const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
|
|
12
|
-
const TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
|
|
13
|
-
const TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
|
|
14
|
-
const AUTH_SCHEME = 'BrainClient'
|
|
15
|
-
const TOKEN_FILE_MAX_BYTES = 16_384
|
|
16
|
-
const POSIX_TOKEN_FILE_MODE = 0o600
|
|
17
|
-
const WINDOWS_BROKER_DIRECTORY = ['CLI.Tax', 'broker']
|
|
18
11
|
const FEEDBACK_COMMENT_MAX = 500
|
|
19
12
|
const EVALUATION_DURATION_MAX = 86_400_000
|
|
20
13
|
const SCORE_MIN = 0
|
|
21
14
|
const SCORE_MAX = 100
|
|
22
|
-
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
|
|
23
15
|
const INVOCATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
24
16
|
const DIGEST_PATTERN = /^[0-9a-f]{64}$/
|
|
25
17
|
const PROTOCOL_STATUSES = new Set(['succeeded', 'blocked', 'failed'])
|
|
@@ -48,84 +40,6 @@ function boundedInteger(value, label) {
|
|
|
48
40
|
return value
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
function insideWindowsDirectory(candidate, directory) {
|
|
52
|
-
const relative = win32.relative(directory, candidate)
|
|
53
|
-
return relative === '' || (!relative.startsWith('..\\') && relative !== '..' && !win32.isAbsolute(relative))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function brainClientTokenPath(environment, platform = process.platform) {
|
|
57
|
-
const configured = requiredString(environment[TOKEN_FILE_ENV], TOKEN_FILE_ENV)
|
|
58
|
-
if (platform !== 'win32') return resolve(configured)
|
|
59
|
-
if (!win32.isAbsolute(configured)) {
|
|
60
|
-
throw new Error('Windows Brain Client token file path must be absolute')
|
|
61
|
-
}
|
|
62
|
-
const localAppData = requiredString(environment.LOCALAPPDATA, 'LOCALAPPDATA')
|
|
63
|
-
const brokerDirectory = win32.resolve(localAppData, ...WINDOWS_BROKER_DIRECTORY)
|
|
64
|
-
const candidate = win32.resolve(configured)
|
|
65
|
-
if (!insideWindowsDirectory(candidate, brokerDirectory)) {
|
|
66
|
-
throw new Error(`Windows Brain Client token file must be inside ${brokerDirectory}`)
|
|
67
|
-
}
|
|
68
|
-
return candidate
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function assertTokenFileStatus(status, platform, currentUserId) {
|
|
72
|
-
if (!status.isFile() || status.size < 1 || status.size > TOKEN_FILE_MAX_BYTES) {
|
|
73
|
-
throw new Error('Brain Client token file must be a non-empty restricted file')
|
|
74
|
-
}
|
|
75
|
-
if (platform === 'win32') return
|
|
76
|
-
if (!Number.isInteger(currentUserId)) {
|
|
77
|
-
throw new Error('Brain Client token file ownership cannot be verified')
|
|
78
|
-
}
|
|
79
|
-
if (status.uid !== currentUserId || (status.mode & 0o777) !== POSIX_TOKEN_FILE_MODE) {
|
|
80
|
-
throw new Error('Brain Client token file must be owned by the current user with mode 0600')
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function parseTokenFile(source) {
|
|
85
|
-
let tokenFile
|
|
86
|
-
try {
|
|
87
|
-
tokenFile = asObject(JSON.parse(source), 'Brain Client token file')
|
|
88
|
-
} catch {
|
|
89
|
-
throw new Error('Brain Client token file must contain valid JSON')
|
|
90
|
-
}
|
|
91
|
-
const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
|
|
92
|
-
if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
|
|
93
|
-
throw new Error('Brain Client token file contains unknown or missing fields')
|
|
94
|
-
}
|
|
95
|
-
return tokenFile
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function brainClientAuthorization(context, environment, dependencies = {}) {
|
|
99
|
-
const platform = dependencies.platform ?? process.platform
|
|
100
|
-
const tokenFilePath = brainClientTokenPath(environment, platform)
|
|
101
|
-
const inspectPath = dependencies.lstat ?? lstat
|
|
102
|
-
const openPath = dependencies.open ?? open
|
|
103
|
-
const currentUserId = platform === 'win32'
|
|
104
|
-
? null
|
|
105
|
-
: (dependencies.getuid ?? process.getuid)?.()
|
|
106
|
-
const linkStatus = await inspectPath(tokenFilePath)
|
|
107
|
-
if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
|
|
108
|
-
const noFollow = platform === 'win32' ? 0 : (constants.O_NOFOLLOW ?? 0)
|
|
109
|
-
const handle = await openPath(tokenFilePath, constants.O_RDONLY | noFollow)
|
|
110
|
-
try {
|
|
111
|
-
const status = await handle.stat()
|
|
112
|
-
assertTokenFileStatus(status, platform, currentUserId)
|
|
113
|
-
const tokenFile = parseTokenFile(await handle.readFile('utf8'))
|
|
114
|
-
const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
|
|
115
|
-
if (tokenFile.schemaVersion !== TOKEN_FILE_VERSION
|
|
116
|
-
|| tokenFile.authorizationScheme !== AUTH_SCHEME
|
|
117
|
-
|| endpoint.origin !== new URL(context.endpoint).origin
|
|
118
|
-
|| endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
|
|
119
|
-
|| endpoint.username || endpoint.password
|
|
120
|
-
|| !TOKEN_PATTERN.test(tokenFile.token)) {
|
|
121
|
-
throw new Error('Brain Client token file authority is invalid')
|
|
122
|
-
}
|
|
123
|
-
return `${AUTH_SCHEME} ${tokenFile.token}`
|
|
124
|
-
} finally {
|
|
125
|
-
await handle.close()
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
43
|
function canonicalJson(value) {
|
|
130
44
|
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
131
45
|
return JSON.stringify(value)
|
|
@@ -205,6 +119,9 @@ export function authoritativeEvaluation(value, expected) {
|
|
|
205
119
|
|
|
206
120
|
async function responsePayload(response, context, request) {
|
|
207
121
|
const label = `${context.displayName} ${request.operation} response`
|
|
122
|
+
if (response.status === 401 || response.status === 403) {
|
|
123
|
+
throw new OfficialSkillResponseError(request, 'http-response', 'Brain Client authorization was rejected; copy the current authenticated setup from CLI.Tax and run configure again. Revoked credentials cannot renew themselves.')
|
|
124
|
+
}
|
|
208
125
|
let payload
|
|
209
126
|
try {
|
|
210
127
|
payload = await response.json()
|
|
@@ -235,6 +152,9 @@ function invocationRequest(context, operation, input) {
|
|
|
235
152
|
}
|
|
236
153
|
|
|
237
154
|
export async function invokeOfficialSkill(context, operation, input, dependencies) {
|
|
155
|
+
const prepared = await prepareOfficialSkillUse(context, 'broker.mjs', dependencies)
|
|
156
|
+
if (prepared.module !== null) return withUpgradeMetadata(
|
|
157
|
+
await prepared.module.invokeOfficialSkill(prepared.context, operation, input, dependencies), prepared.upgrade)
|
|
238
158
|
const environment = asObject(dependencies.environment, 'broker environment')
|
|
239
159
|
if (typeof dependencies.request !== 'function') {
|
|
240
160
|
throw new Error('broker request dependency is required')
|
|
@@ -252,14 +172,14 @@ export async function invokeOfficialSkill(context, operation, input, dependencie
|
|
|
252
172
|
})
|
|
253
173
|
} catch (error) {
|
|
254
174
|
const failure = new OfficialSkillInvocationError(context, requestEnvelope, transportFailureCode(error), 'request', error?.transport)
|
|
255
|
-
return recoverTransportFailure(context, requestEnvelope, dependencies, authorization, failure)
|
|
175
|
+
return withUpgradeMetadata(await recoverTransportFailure(context, requestEnvelope, dependencies, authorization, failure), prepared.upgrade)
|
|
256
176
|
}
|
|
257
177
|
let payload
|
|
258
178
|
try {
|
|
259
179
|
payload = await responsePayload(response, context, requestEnvelope)
|
|
260
180
|
} catch (error) {
|
|
261
181
|
if (!(error instanceof OfficialSkillInvocationError)) throw error
|
|
262
|
-
return recoverTransportFailure(context, requestEnvelope, dependencies, authorization, error)
|
|
182
|
+
return withUpgradeMetadata(await recoverTransportFailure(context, requestEnvelope, dependencies, authorization, error), prepared.upgrade)
|
|
263
183
|
}
|
|
264
184
|
if (!response.ok || payload.ok !== true) {
|
|
265
185
|
throw new OfficialSkillResponseError(requestEnvelope, 'http-response', `${context.displayName} ${operation} failed: HTTP ${response.status}`)
|
|
@@ -267,7 +187,7 @@ export async function invokeOfficialSkill(context, operation, input, dependencie
|
|
|
267
187
|
try {
|
|
268
188
|
const invocation = validateInvocationResponse(context, operation, payload, requestEnvelope)
|
|
269
189
|
const transport = transportDiagnostics(response.transport)
|
|
270
|
-
return transport ? { ...invocation, transport } : invocation
|
|
190
|
+
return withUpgradeMetadata(transport ? { ...invocation, transport } : invocation, prepared.upgrade)
|
|
271
191
|
} catch (error) {
|
|
272
192
|
throw new OfficialSkillResponseError(requestEnvelope, 'response-validation',
|
|
273
193
|
error instanceof Error ? error.message : 'Skill response validation failed')
|
package/cli.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { TASKS_USAGE, runTasksCli } from './aimlock-tasks-cli.mjs'
|
|
2
3
|
import { realpathSync } from 'node:fs'
|
|
3
4
|
import { dirname, resolve } from 'node:path'
|
|
4
5
|
import { cwd, stdin, stdout } from 'node:process'
|
|
@@ -95,7 +96,7 @@ export function localAimlockApplicability(facts) {
|
|
|
95
96
|
export function aimlockUsage(context) {
|
|
96
97
|
const usage = defaultUsage(context)
|
|
97
98
|
if (!usage.includes(COMMON_RUN_USAGE)) throw new Error('Shared CLI run usage contract changed')
|
|
98
|
-
return usage.replace(COMMON_RUN_USAGE, AIMLOCK_RUN_USAGE) + '\n\n' + CHAIN_USAGE
|
|
99
|
+
return usage.replace(COMMON_RUN_USAGE, AIMLOCK_RUN_USAGE) + '\n\n' + CHAIN_USAGE + '\n\n' + TASKS_USAGE
|
|
99
100
|
+ '\n\nRead-time renewal: local budget-auto-renew-request <repositoryRoot> prepares one Confirm Protocol approval;'
|
|
100
101
|
+ '\nlocal budget-auto-renew activates the approved chain/scope/policy; budget-auto-renew-stop revokes or completes it.'
|
|
101
102
|
}
|
|
@@ -196,6 +197,8 @@ const cliPath = fileURLToPath(import.meta.url)
|
|
|
196
197
|
if (process.argv[1] && realpathSync(resolve(process.argv[1])) === cliPath) {
|
|
197
198
|
if (process.argv[2] === 'brain') {
|
|
198
199
|
await runBrainCli(process.argv.slice(3))
|
|
200
|
+
} else if (process.argv[2] === 'tasks') {
|
|
201
|
+
await runTasksCli(process.argv.slice(3))
|
|
199
202
|
} else if (process.argv[2] === 'chain') {
|
|
200
203
|
await runChainCli(process.argv.slice(3))
|
|
201
204
|
} else if (process.argv[2] === 'local') {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { cp, lstat, mkdir, readdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
import { assertAccountAncestors, currentAccountHome } from './broker-account-storage.mjs'
|
|
8
|
+
|
|
9
|
+
const runFile = promisify(execFile)
|
|
10
|
+
const INSTALL_META = 'install-meta.json'
|
|
11
|
+
const MAX_SKILL_FILES = 256
|
|
12
|
+
const GIT_TIMEOUT_MS = 8000
|
|
13
|
+
|
|
14
|
+
function object(value, label) {
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(label + ' must be an object')
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readInstallMeta(target) {
|
|
20
|
+
const path = join(target, INSTALL_META)
|
|
21
|
+
if (!existsSync(path)) return null
|
|
22
|
+
return object(JSON.parse(readFileSync(path, 'utf8')), INSTALL_META)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function installTarget(skillName, explicit, environment = process.env, workingDirectory = process.cwd()) {
|
|
26
|
+
if (explicit !== undefined) {
|
|
27
|
+
if (typeof explicit !== 'string' || !explicit.trim()) throw new Error('Install directory must be non-empty')
|
|
28
|
+
return resolve(explicit)
|
|
29
|
+
}
|
|
30
|
+
if (environment.CODEX_HOME !== undefined) {
|
|
31
|
+
if (typeof environment.CODEX_HOME !== 'string' || !isAbsolute(environment.CODEX_HOME)) throw new Error('CODEX_HOME must be absolute')
|
|
32
|
+
return join(environment.CODEX_HOME, 'skills', skillName)
|
|
33
|
+
}
|
|
34
|
+
return join(workingDirectory, '.codex', 'skills', skillName)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function contained(root, path) {
|
|
38
|
+
const inside = relative(root, path)
|
|
39
|
+
return inside === '' || (!inside.startsWith('..' + '/') && inside !== '..' && !isAbsolute(inside)
|
|
40
|
+
&& !inside.startsWith('..' + '\\'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sameIdentity(meta, context) {
|
|
44
|
+
return meta.source === context.runtimeCode && meta.slug === context.skillName && meta.endpoint === context.endpoint
|
|
45
|
+
&& typeof meta.version === 'string' && /^v?\d+\.\d+\.\d+$/.test(meta.version)
|
|
46
|
+
&& typeof meta.packageVersion === 'string' && /^\d+\.\d+\.\d+$/.test(meta.packageVersion)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function newerVersion(installed, selected) {
|
|
50
|
+
const parts = version => {
|
|
51
|
+
if (typeof version !== 'string' || !/^v?\d+\.\d+\.\d+$/.test(version)) {
|
|
52
|
+
throw new Error('Managed skill comparison requires a semantic release version')
|
|
53
|
+
}
|
|
54
|
+
return version.replace(/^v/, '').split('.').map(value => BigInt(value))
|
|
55
|
+
}
|
|
56
|
+
const left = parts(installed), right = parts(selected)
|
|
57
|
+
const index = left.findIndex((part, position) => part !== right[position])
|
|
58
|
+
return index !== -1 && left[index] > right[index]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertNoManagedDowngrade(previous, context) {
|
|
62
|
+
if (previous !== null && (newerVersion(previous.version, context.skillVersion)
|
|
63
|
+
|| newerVersion(previous.packageVersion, context.packageVersion))) {
|
|
64
|
+
throw new Error('The managed skill is newer than the selected release; refusing to downgrade its installation')
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function targetExists(path) {
|
|
69
|
+
try { return await lstat(path) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function trackedSource(target) {
|
|
73
|
+
let current = dirname(target)
|
|
74
|
+
for (;;) {
|
|
75
|
+
if (await targetExists(join(current, '.git')) !== null) {
|
|
76
|
+
try {
|
|
77
|
+
const result = await runFile('git', ['-C', current, 'ls-files', '--', relative(current, target)],
|
|
78
|
+
{ timeout: GIT_TIMEOUT_MS, maxBuffer: 1_048_576 })
|
|
79
|
+
return result.stdout.trim().length > 0
|
|
80
|
+
} catch { throw new Error('Cannot verify whether the install target contains repository source files') }
|
|
81
|
+
}
|
|
82
|
+
const parent = dirname(current)
|
|
83
|
+
if (parent === current) return false
|
|
84
|
+
current = parent
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function managedTarget(context, target, allowCreate) {
|
|
89
|
+
const path = resolve(target)
|
|
90
|
+
await assertAccountAncestors(dirname(path))
|
|
91
|
+
if (contained(resolve(context.packageRoot), path) || contained(path, resolve(context.packageRoot))) {
|
|
92
|
+
return { path, status: 'skipped', reason: 'package-source' }
|
|
93
|
+
}
|
|
94
|
+
const status = await targetExists(path)
|
|
95
|
+
if (status === null) return allowCreate ? { path, status: 'new', meta: null }
|
|
96
|
+
: { path, status: 'skipped', reason: 'not-installed' }
|
|
97
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new Error('Installed skill target must be a regular directory')
|
|
98
|
+
if (await trackedSource(path)) return { path, status: 'skipped', reason: 'repository-source' }
|
|
99
|
+
const metaStatus = await targetExists(join(path, INSTALL_META))
|
|
100
|
+
if (metaStatus === null) return { path, status: 'skipped', reason: 'unmanaged' }
|
|
101
|
+
if (!metaStatus.isFile() || metaStatus.isSymbolicLink()) throw new Error('Install metadata must be a regular file')
|
|
102
|
+
if (process.platform !== 'win32' && (status.uid !== process.getuid() || metaStatus.uid !== process.getuid())) throw new Error('Managed skill installation must belong to the current account')
|
|
103
|
+
const meta = readInstallMeta(path)
|
|
104
|
+
if (!sameIdentity(meta, context)) return { path, status: 'skipped', reason: 'identity-mismatch' }
|
|
105
|
+
return { path, status: 'managed', meta }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function verifySkillSource(directory, context) {
|
|
109
|
+
let files = 0
|
|
110
|
+
async function visit(path) {
|
|
111
|
+
const status = await lstat(path)
|
|
112
|
+
if (status.isSymbolicLink()) throw new Error('Skill package cannot contain symbolic links')
|
|
113
|
+
if (status.isDirectory()) {
|
|
114
|
+
for (const entry of await readdir(path)) await visit(join(path, entry))
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if (!status.isFile() || ++files > MAX_SKILL_FILES) throw new Error('Skill package contains an unsupported or excessive file set')
|
|
118
|
+
}
|
|
119
|
+
await assertAccountAncestors(directory)
|
|
120
|
+
await visit(directory)
|
|
121
|
+
const skill = object(JSON.parse(readFileSync(join(directory, 'skill.json'), 'utf8')), 'skill.json')
|
|
122
|
+
if (skill.name !== context.skillName || skill.endpoint !== context.endpoint || skill.version !== context.skillVersion) {
|
|
123
|
+
throw new Error('Skill package documentation identity does not match the selected version')
|
|
124
|
+
}
|
|
125
|
+
const markdown = await lstat(join(directory, 'SKILL.md'))
|
|
126
|
+
if (!markdown.isFile() || markdown.size === 0) throw new Error('Skill package must contain its actual SKILL.md')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function restoreInstallation(target, backup, placed, renamePath) {
|
|
130
|
+
if (placed) await rm(target, { recursive: true })
|
|
131
|
+
if (backup !== null) await renamePath(backup, target)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function replaceInstallation(context, target, previous, dependencies) {
|
|
135
|
+
const renamePath = dependencies.rename === undefined ? rename : dependencies.rename
|
|
136
|
+
const suffix = randomUUID()
|
|
137
|
+
const stage = join(dirname(target), '.' + context.skillName + '.stage-' + suffix)
|
|
138
|
+
const backup = previous === null ? null : join(dirname(target), '.' + context.skillName + '.backup-' + suffix)
|
|
139
|
+
let moved = false, placed = false
|
|
140
|
+
await mkdir(stage, { mode: 0o700 })
|
|
141
|
+
try {
|
|
142
|
+
await verifySkillSource(context.skillDir, context)
|
|
143
|
+
for (const entry of await readdir(context.skillDir)) {
|
|
144
|
+
await cp(join(context.skillDir, entry), join(stage, entry), { recursive: true, force: false, errorOnExist: true })
|
|
145
|
+
}
|
|
146
|
+
await writeFile(join(stage, INSTALL_META), JSON.stringify({ source: context.runtimeCode, slug: context.skillName,
|
|
147
|
+
version: context.skillVersion, packageVersion: context.packageVersion, endpoint: context.endpoint,
|
|
148
|
+
installedAt: new Date().toISOString() }) + '\n', { flag: 'wx', mode: 0o600 })
|
|
149
|
+
await verifySkillSource(stage, context)
|
|
150
|
+
if (backup !== null) { await renamePath(target, backup); moved = true }
|
|
151
|
+
await renamePath(stage, target)
|
|
152
|
+
placed = true
|
|
153
|
+
return { path: target, status: previous === null ? 'installed' : 'updated',
|
|
154
|
+
previousVersion: previous === null ? null : previous.version, version: context.skillVersion,
|
|
155
|
+
documentationPath: join(target, 'SKILL.md'), backupPath: backup }
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (moved || placed) await restoreInstallation(target, moved ? backup : null, placed, renamePath)
|
|
158
|
+
throw error
|
|
159
|
+
} finally {
|
|
160
|
+
const remaining = await targetExists(stage)
|
|
161
|
+
if (remaining !== null) await rm(stage, { recursive: true })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function writeManagedSkill(context, target, options = {}) {
|
|
166
|
+
const allowCreate = options.allowCreate === true
|
|
167
|
+
const candidate = await managedTarget(context, target, allowCreate)
|
|
168
|
+
if (candidate.status === 'skipped') return candidate
|
|
169
|
+
assertNoManagedDowngrade(candidate.meta, context)
|
|
170
|
+
if (candidate.meta !== null && candidate.meta.version === context.skillVersion
|
|
171
|
+
&& candidate.meta.packageVersion === context.packageVersion) {
|
|
172
|
+
await verifySkillSource(candidate.path, context)
|
|
173
|
+
return { path: candidate.path, status: 'current', version: context.skillVersion,
|
|
174
|
+
documentationPath: join(candidate.path, 'SKILL.md') }
|
|
175
|
+
}
|
|
176
|
+
await mkdir(dirname(candidate.path), { recursive: true })
|
|
177
|
+
await assertAccountAncestors(dirname(candidate.path))
|
|
178
|
+
const lock = join(dirname(candidate.path), '.' + context.skillName + '.install-lock')
|
|
179
|
+
try { await mkdir(lock, { mode: 0o700 }) } catch (error) {
|
|
180
|
+
if (error.code === 'EEXIST') throw new Error('Skill installation is already locked; inspect its pending update before retrying')
|
|
181
|
+
throw error
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const checked = await managedTarget(context, candidate.path, allowCreate)
|
|
185
|
+
if (checked.status === 'skipped') throw new Error('Skill installation ownership changed before update')
|
|
186
|
+
assertNoManagedDowngrade(checked.meta, context)
|
|
187
|
+
return await replaceInstallation(context, checked.path, checked.meta, options)
|
|
188
|
+
} finally { await rm(lock, { recursive: true }) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function refreshManagedSkillCopies(context, dependencies = {}) {
|
|
192
|
+
const environment = dependencies.environment === undefined ? process.env : dependencies.environment
|
|
193
|
+
const home = dependencies.homeDirectory === undefined ? currentAccountHome() : dependencies.homeDirectory
|
|
194
|
+
const workingDirectory = dependencies.workingDirectory === undefined ? process.cwd() : dependencies.workingDirectory
|
|
195
|
+
if (!isAbsolute(home) || !isAbsolute(workingDirectory)) throw new Error('Account and project directories must be absolute')
|
|
196
|
+
const accountCodex = environment.CODEX_HOME === undefined ? join(home, '.codex') : environment.CODEX_HOME
|
|
197
|
+
if (typeof accountCodex !== 'string' || !isAbsolute(accountCodex) || !contained(home, accountCodex)) {
|
|
198
|
+
throw new Error('Automatic skill refresh requires CODEX_HOME inside the current account home')
|
|
199
|
+
}
|
|
200
|
+
const targets = [...new Set([join(workingDirectory, '.codex', 'skills', context.skillName),
|
|
201
|
+
join(accountCodex, 'skills', context.skillName)])]
|
|
202
|
+
const results = []
|
|
203
|
+
for (const target of targets) results.push(await writeManagedSkill(context, target, dependencies))
|
|
204
|
+
return results
|
|
205
|
+
}
|
package/installer.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { latestOfficialSkillContext } from './official-skill-update.mjs'
|
|
2
|
+
import { installTarget, writeManagedSkill } from './installer-storage.mjs'
|
|
3
|
+
export { installTarget, readInstallMeta } from './installer-storage.mjs'
|
|
4
|
+
import { configureBrainClientCredential } from './broker-credentials.mjs'
|
|
1
5
|
/**
|
|
2
6
|
* 八个官方技能共用这一份安装器。packages/*-cli/installer.mjs 必须与本文件字节一致。
|
|
3
7
|
* 禁止第二套超时、第二套版本来源、第二套 bin 名。
|
|
4
8
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { dirname, join
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
import { writeFile } from 'node:fs/promises'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
8
12
|
import { stdin, stdout } from 'node:process'
|
|
9
13
|
import { createInterface } from 'node:readline/promises'
|
|
10
14
|
import { fileURLToPath } from 'node:url'
|
|
@@ -33,7 +37,6 @@ export {
|
|
|
33
37
|
|
|
34
38
|
import { createBrokerTransport } from './broker-transport.mjs'
|
|
35
39
|
|
|
36
|
-
const INSTALL_META = 'install-meta.json'
|
|
37
40
|
const BROKER_STDIN_MAX_BYTES = 1_048_576
|
|
38
41
|
|
|
39
42
|
function asObject(value, label) {
|
|
@@ -81,19 +84,6 @@ export function loadOfficialSkillContext(packageRoot) {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
export function readInstallMeta(target) {
|
|
85
|
-
const path = join(target, INSTALL_META)
|
|
86
|
-
if (!existsSync(path)) return null
|
|
87
|
-
return asObject(JSON.parse(readFileSync(path, 'utf8')), INSTALL_META)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function installTarget(skillName, explicit) {
|
|
91
|
-
if (explicit) return resolve(explicit)
|
|
92
|
-
const codexHome = process.env.CODEX_HOME?.trim()
|
|
93
|
-
if (codexHome) return join(codexHome, 'skills', skillName)
|
|
94
|
-
return join(process.cwd(), '.codex', 'skills', skillName)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
87
|
export async function fetchLatestVersion(context) {
|
|
98
88
|
const request = createBrokerTransport({ environment: process.env })
|
|
99
89
|
const response = await request(context.latestEndpoint, { signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) })
|
|
@@ -105,51 +95,38 @@ export async function fetchLatestVersion(context) {
|
|
|
105
95
|
}
|
|
106
96
|
}
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
await rm(join(target, 'references'), { recursive: true, force: true })
|
|
113
|
-
await cp(context.skillDir, target, { recursive: true, force: true })
|
|
114
|
-
const installed = asObject(JSON.parse(readFileSync(join(target, 'skill.json'), 'utf8')), 'installed skill.json')
|
|
115
|
-
const installedVersion = requiredString(installed.version, 'installed skill.json version')
|
|
116
|
-
await writeFile(join(target, INSTALL_META), `${JSON.stringify({
|
|
117
|
-
source: context.runtimeCode,
|
|
118
|
-
slug: context.skillName,
|
|
119
|
-
version: installedVersion,
|
|
120
|
-
packageVersion: context.packageVersion,
|
|
121
|
-
endpoint: context.endpoint,
|
|
122
|
-
installedAt: new Date().toISOString(),
|
|
123
|
-
}, null, 2)}\n`)
|
|
124
|
-
if (previous?.version && previous.version !== installedVersion) {
|
|
125
|
-
console.log(`${context.displayName} skill updated: ${target}`)
|
|
126
|
-
console.log(` ${previous.version} → ${installedVersion}`)
|
|
127
|
-
} else {
|
|
128
|
-
console.log(`${context.displayName} skill installed: ${target} (${installedVersion})`)
|
|
129
|
-
}
|
|
130
|
-
console.log('Next: return to your IDE and state the goal. The agent reads the installed SKILL.md.')
|
|
98
|
+
function writeInstallationResult(runtime, value) {
|
|
99
|
+
const line = JSON.stringify(value) + '\n'
|
|
100
|
+
if (runtime.writeOutput === undefined) process.stdout.write(line)
|
|
101
|
+
else runtime.writeOutput(line)
|
|
131
102
|
}
|
|
132
103
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
process.
|
|
104
|
+
function installationDependencies(dependencies) {
|
|
105
|
+
return dependencies === undefined ? brokerDependencies() : dependencies
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function installOfficialSkill(context, explicit, dependencies) {
|
|
109
|
+
const runtime = installationDependencies(dependencies)
|
|
110
|
+
const selected = await latestOfficialSkillContext(context, runtime)
|
|
111
|
+
const environment = runtime.environment === undefined ? process.env : runtime.environment
|
|
112
|
+
const workingDirectory = runtime.workingDirectory === undefined ? process.cwd() : runtime.workingDirectory
|
|
113
|
+
const target = installTarget(selected.skillName, explicit, environment, workingDirectory)
|
|
114
|
+
const installed = await writeManagedSkill(selected, target, { ...runtime, allowCreate: true })
|
|
115
|
+
if (installed.status === 'skipped') throw new Error('Skill install refused: ' + installed.reason)
|
|
116
|
+
writeInstallationResult(runtime, { installed, reloadRequired: installed.status !== 'current' })
|
|
117
|
+
return installed
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function checkOfficialSkill(context, explicit, dependencies) {
|
|
121
|
+
const runtime = installationDependencies(dependencies)
|
|
122
|
+
const selected = await latestOfficialSkillContext(context, runtime)
|
|
123
|
+
const environment = runtime.environment === undefined ? process.env : runtime.environment
|
|
124
|
+
const workingDirectory = runtime.workingDirectory === undefined ? process.cwd() : runtime.workingDirectory
|
|
125
|
+
const target = installTarget(selected.skillName, explicit, environment, workingDirectory)
|
|
126
|
+
const installed = await writeManagedSkill(selected, target, runtime)
|
|
127
|
+
if (installed.status === 'skipped') throw new Error('Skill check could not update its managed target: ' + installed.reason)
|
|
128
|
+
writeInstallationResult(runtime, { installed, reloadRequired: installed.status === 'updated' })
|
|
129
|
+
return installed
|
|
153
130
|
}
|
|
154
131
|
|
|
155
132
|
export function defaultUsage(context, extraLines) {
|
|
@@ -157,10 +134,12 @@ export function defaultUsage(context, extraLines) {
|
|
|
157
134
|
`${context.npmName} — install and run the ${context.displayName} skill from CLI.Tax`,
|
|
158
135
|
'',
|
|
159
136
|
'Usage:',
|
|
137
|
+
' configure < credential.json',
|
|
138
|
+
' Store the Brain Client credential for this account; never put tokens in command arguments.',
|
|
160
139
|
` npx ${context.npmName}@latest install [directory]`,
|
|
161
140
|
` Install the ${context.displayName} skill for the current IDE.`,
|
|
162
141
|
` npx ${context.npmName}@latest check [directory]`,
|
|
163
|
-
' Check
|
|
142
|
+
' Check and atomically update an already managed skill to the current official release.',
|
|
164
143
|
` npx ${context.npmName}@latest run`,
|
|
165
144
|
" Run this skill's applicability or onboarding flow; only a real HTTP invocation can trigger automatic evaluation.",
|
|
166
145
|
` npx ${context.npmName}@latest invoke <operation> <JSON-object>`,
|
|
@@ -169,7 +148,8 @@ export function defaultUsage(context, extraLines) {
|
|
|
169
148
|
' Read one {"operation":"...","input":{...}} request from JSON stdin.',
|
|
170
149
|
` npx ${context.npmName}@latest recover <operation> <requestId>`,
|
|
171
150
|
' Query an uncertain invocation without resending or charging again.',
|
|
172
|
-
'Credential:
|
|
151
|
+
'Credential: configure reads a token-file JSON document from stdin and stores it once for the current account.',
|
|
152
|
+
'An explicit CLITAX_BRAIN_CLIENT_TOKEN_FILE must be absolute. Revoked keys require a fresh authenticated copy from CLI.Tax.',
|
|
173
153
|
`Endpoint: ${context.endpoint}`,
|
|
174
154
|
]
|
|
175
155
|
if (extraLines?.length) lines.push('', ...extraLines)
|
|
@@ -270,7 +250,12 @@ export async function dispatchOfficialSkillCli(options) {
|
|
|
270
250
|
const command = args[0] ?? 'help'
|
|
271
251
|
const argument = args[1]
|
|
272
252
|
try {
|
|
273
|
-
if (command === '
|
|
253
|
+
if (command === 'configure') {
|
|
254
|
+
if (args.length !== 1) throw new Error('configure accepts credentials only through JSON stdin')
|
|
255
|
+
const configured = await configureBrainClientCredential(await readBrokerSource(stdin))
|
|
256
|
+
process.stdout.write(JSON.stringify(configured) + '\n')
|
|
257
|
+
}
|
|
258
|
+
else if (command === 'install') await installOfficialSkill(context, argument)
|
|
274
259
|
else if (command === 'check') await checkOfficialSkill(context, argument)
|
|
275
260
|
else if (command === 'run') await options.runCommand(context)
|
|
276
261
|
else if (command === 'recover') await runBrokerRecovery(context, args)
|