@skrr-ai/cli 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +2 -2
- package/dist/base-command.js +5 -0
- package/dist/commands/agents/list.d.ts +2 -1
- package/dist/commands/agents/list.js +2 -1
- package/dist/commands/agents/show.js +2 -1
- package/dist/commands/balance/statement.js +8 -4
- package/dist/commands/browser/install.js +6 -2
- package/dist/commands/browser/uninstall.js +6 -2
- package/dist/commands/code/acp.js +2 -1
- package/dist/commands/code/doctor.d.ts +1 -1
- package/dist/commands/code/doctor.js +6 -5
- package/dist/commands/code/handover.d.ts +2 -2
- package/dist/commands/code/handover.js +2 -2
- package/dist/commands/code/index.d.ts +2 -2
- package/dist/commands/code/index.js +18 -17
- package/dist/commands/code/install.d.ts +3 -3
- package/dist/commands/code/install.js +18 -11
- package/dist/commands/code/migrate.js +3 -2
- package/dist/commands/code/oversky-agent.d.ts +5 -4
- package/dist/commands/code/oversky-agent.js +24 -22
- package/dist/commands/code/run.js +2 -1
- package/dist/commands/harnesses/list.d.ts +9 -1
- package/dist/commands/harnesses/list.js +28 -7
- package/dist/commands/harnesses/show.js +7 -2
- package/dist/commands/instructions/install.js +16 -6
- package/dist/commands/learning/delete.d.ts +9 -0
- package/dist/commands/learning/delete.js +40 -0
- package/dist/commands/learning/export.d.ts +9 -0
- package/dist/commands/learning/export.js +56 -0
- package/dist/commands/learning/reset.d.ts +9 -0
- package/dist/commands/learning/reset.js +36 -0
- package/dist/commands/learning/set.d.ts +10 -0
- package/dist/commands/learning/set.js +52 -0
- package/dist/commands/learning/status.d.ts +8 -0
- package/dist/commands/learning/status.js +35 -0
- package/dist/commands/logout.js +10 -5
- package/dist/commands/machines/dedicated/attach.js +2 -1
- package/dist/commands/tasks/runs.js +3 -1
- package/dist/commands/workspaces/use.js +2 -1
- package/dist/lib/adaptive-learning.d.ts +37 -0
- package/dist/lib/adaptive-learning.js +39 -0
- package/dist/lib/auth-core-init.d.ts +23 -0
- package/dist/lib/auth-core-init.js +61 -0
- package/dist/lib/code-handover.d.ts +1 -1
- package/dist/lib/code-handover.js +3 -2
- package/dist/lib/config.d.ts +30 -4
- package/dist/lib/config.js +2 -1
- package/dist/lib/daemon-installer.d.ts +1 -1
- package/dist/lib/daemon-target.js +2 -2
- package/dist/lib/dedicated-machines.d.ts +2 -2
- package/dist/lib/dedicated-machines.js +11 -4
- package/dist/lib/exec-runtime-binary.d.ts +2 -2
- package/dist/lib/exec-runtime-binary.js +2 -2
- package/dist/lib/{sky-code-agent.d.ts → first-party-harness-agent.d.ts} +20 -19
- package/dist/lib/{sky-code-agent.js → first-party-harness-agent.js} +85 -47
- package/dist/lib/{sky-code-broker.d.ts → first-party-harness-broker.d.ts} +6 -6
- package/dist/lib/{sky-code-broker.js → first-party-harness-broker.js} +23 -23
- package/dist/lib/{sky-code-doctor.d.ts → first-party-harness-doctor.d.ts} +12 -2
- package/dist/lib/{sky-code-doctor.js → first-party-harness-doctor.js} +181 -94
- package/dist/lib/{sky-code-managed.d.ts → first-party-harness-managed.d.ts} +55 -30
- package/dist/lib/{sky-code-managed.js → first-party-harness-managed.js} +92 -35
- package/dist/lib/{sky-code.d.ts → first-party-harness.d.ts} +107 -36
- package/dist/lib/{sky-code.js → first-party-harness.js} +172 -73
- package/dist/lib/harness-provider-input.d.ts +35 -0
- package/dist/lib/harness-provider-input.js +57 -0
- package/dist/lib/harness-tiers.d.ts +8 -2
- package/dist/lib/harness-tiers.js +11 -6
- package/dist/lib/task-instruction-offer.js +10 -4
- package/dist/lib/worklogs.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +248 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +361 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +180 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.d.ts +111 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +314 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.d.ts +11 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +14 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +51 -14
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +4 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +4 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +248 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +332 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +170 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.d.ts +111 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +303 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.d.ts +11 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +14 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +12 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +4 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +4 -4
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +4816 -4782
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/index.js +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.js +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.js +1 -1
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +34286 -33962
- package/package.json +5 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.d.ts +0 -96
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.js +0 -115
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.d.ts +0 -96
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.js +0 -107
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the first-party harness lives on disk, and the one-time move that keeps
|
|
3
|
+
* that true across a rename.
|
|
4
|
+
*
|
|
5
|
+
* ── Why one implementation ────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* Three processes put an engine under the skrr root and read it back: the daemon
|
|
8
|
+
* installer, the CLI (`skrr code`), and the desktop app's bundled-engine
|
|
9
|
+
* install. Each used to build `<root>/sky-code/bin/sky-code` itself, and they
|
|
10
|
+
* already disagreed once about which root that was (see `configRoot.ts`). A
|
|
11
|
+
* rename that every one of them had to perform independently would disagree
|
|
12
|
+
* again, so the paths AND the move live here.
|
|
13
|
+
*
|
|
14
|
+
* ── The move ──────────────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* Idempotent, lossless, and safe to run concurrently from all three:
|
|
17
|
+
*
|
|
18
|
+
* 1. The HOME follows the engine's namespace. If `<root>/<appName>` does not
|
|
19
|
+
* exist and `<root>/<legacyAppName>` is a real directory, rename it and
|
|
20
|
+
* leave the legacy name as a relative symlink. (No legacy app name exists
|
|
21
|
+
* yet; this step is what an engine-namespace rename will use.)
|
|
22
|
+
* 2. The BINARY follows the provider. Inside `<home>/bin`, if the canonical
|
|
23
|
+
* binary does not exist and an alias binary is a real file, rename it and
|
|
24
|
+
* each companion (rollback copy, provenance sidecar), leaving a relative
|
|
25
|
+
* symlink at every old name.
|
|
26
|
+
*
|
|
27
|
+
* The two follow different names on purpose: the home is shared with the
|
|
28
|
+
* engine, which reads its config and managed `AGENTS.md` from it, so it can only
|
|
29
|
+
* move when the engine's namespace does (`firstPartyHarnessHomeDirname`). The
|
|
30
|
+
* symlinks are the compatibility story, not an afterthought: every CLI and
|
|
31
|
+
* daemon already installed reads the legacy binary path, and a symlink keeps
|
|
32
|
+
* them working with no coordination.
|
|
33
|
+
*
|
|
34
|
+
* It never overwrites, never deletes, and never renames THROUGH a symlink, so
|
|
35
|
+
* running it twice, or after a downgrade, changes nothing. Any error leaves the
|
|
36
|
+
* tree exactly as it was and is reported rather than thrown: a failed move must
|
|
37
|
+
* degrade to reading the old path, not to an engine that cannot start.
|
|
38
|
+
*
|
|
39
|
+
* Skipped on Windows, where creating a symlink needs a privilege a normal user
|
|
40
|
+
* does not hold, and where no engine artifact is published anyway.
|
|
41
|
+
*/
|
|
42
|
+
import { lstatSync, readlinkSync, renameSync, symlinkSync } from 'node:fs';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
import { resolveConfigRoot } from './configRoot.js';
|
|
45
|
+
import { FIRST_PARTY_HARNESS, FIRST_PARTY_HARNESS_ENGINE, firstPartyHarnessBinaryName, firstPartyHarnessHomeDirname, } from './firstPartyHarness.js';
|
|
46
|
+
/** Suffix of the rollback copy the installer keeps beside the binary. */
|
|
47
|
+
export const FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX = '.previous';
|
|
48
|
+
/**
|
|
49
|
+
* Suffix of the provenance record beside the binary. The `oversky` in it is a
|
|
50
|
+
* persisted file name that already exists on machines, not the product name.
|
|
51
|
+
*/
|
|
52
|
+
export const FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX = '.oversky-provenance.json';
|
|
53
|
+
/** `<root>/<engine appName>` — the engine home, shared by the platform and the engine. */
|
|
54
|
+
export function firstPartyHarnessHomeDir(env = process.env) {
|
|
55
|
+
return path.join(resolveConfigRoot(env), firstPartyHarnessHomeDirname());
|
|
56
|
+
}
|
|
57
|
+
/** `<home>/bin`, where the platform installs the engine binary. */
|
|
58
|
+
export function firstPartyHarnessBinDir(env = process.env) {
|
|
59
|
+
return path.join(firstPartyHarnessHomeDir(env), 'bin');
|
|
60
|
+
}
|
|
61
|
+
/** The installed binary path for a spelling (canonical by default) — lookup order is canonical first. */
|
|
62
|
+
export function firstPartyHarnessInstalledBinaryPath(env = process.env, provider = FIRST_PARTY_HARNESS.provider, platform = process.platform) {
|
|
63
|
+
return path.join(firstPartyHarnessBinDir(env), firstPartyHarnessBinaryName(provider, platform));
|
|
64
|
+
}
|
|
65
|
+
const NODE_FS = {
|
|
66
|
+
lstat(p) {
|
|
67
|
+
try {
|
|
68
|
+
return lstatSync(p);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
readlink(p) {
|
|
75
|
+
try {
|
|
76
|
+
return lstatSync(p).isSymbolicLink() ? readlinkSync(p) : null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
rename: renameSync,
|
|
83
|
+
symlink(target, at, type) {
|
|
84
|
+
symlinkSync(target, at, type);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
function isRealDirectory(stat) {
|
|
88
|
+
return stat !== null && stat.isDirectory() && !stat.isSymbolicLink();
|
|
89
|
+
}
|
|
90
|
+
function isRealFile(stat) {
|
|
91
|
+
return stat !== null && stat.isFile() && !stat.isSymbolicLink();
|
|
92
|
+
}
|
|
93
|
+
function describe(err) {
|
|
94
|
+
return err instanceof Error ? err.message : String(err);
|
|
95
|
+
}
|
|
96
|
+
/** Companions that travel with the binary, binary first. */
|
|
97
|
+
const COMPANION_SUFFIXES = [
|
|
98
|
+
'',
|
|
99
|
+
FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX,
|
|
100
|
+
FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX,
|
|
101
|
+
];
|
|
102
|
+
/**
|
|
103
|
+
* How much newer an alias binary must be before it is treated as a later
|
|
104
|
+
* install rather than the same one. Clock and filesystem-granularity slack.
|
|
105
|
+
*/
|
|
106
|
+
const NEWER_THRESHOLD_MS = 1000;
|
|
107
|
+
/**
|
|
108
|
+
* Move a legacy engine home and binary onto the canonical names. See the file
|
|
109
|
+
* header for the exact contract. Returns what it did; never throws.
|
|
110
|
+
*
|
|
111
|
+
* Inside `<home>/bin` it resolves the three layouts a rename can leave:
|
|
112
|
+
*
|
|
113
|
+
* - canonical ABSENT, an alias is a real file — the ordinary legacy install.
|
|
114
|
+
* The alias moves to canonical and a link is left at its name.
|
|
115
|
+
* - canonical is a LINK to an alias that is a real file — what an alias link
|
|
116
|
+
* left DURING the expand phase becomes once the flip swaps which spelling is
|
|
117
|
+
* canonical. Leaving it means the next install writes its provenance record
|
|
118
|
+
* THROUGH the link onto the old binary's record, and the legacy name then
|
|
119
|
+
* verifies as `tampered`. The real file is adopted onto the canonical name
|
|
120
|
+
* (a rename replaces the link itself, never what it points at).
|
|
121
|
+
* - canonical and an alias are BOTH real files and the alias is newer — a
|
|
122
|
+
* client that predates the flip installed an update under the old name. The
|
|
123
|
+
* newer engine is what every reader should run, so it is promoted: the older
|
|
124
|
+
* canonical binary becomes the rollback copy, and the alias becomes a link.
|
|
125
|
+
*
|
|
126
|
+
* Any other layout is left exactly as it is.
|
|
127
|
+
*/
|
|
128
|
+
export function migrateFirstPartyHarnessHome(options = {}) {
|
|
129
|
+
const env = options.env ?? process.env;
|
|
130
|
+
const platform = options.platform ?? process.platform;
|
|
131
|
+
const fs = options.fs ?? NODE_FS;
|
|
132
|
+
if (platform === 'win32') {
|
|
133
|
+
return { moved: [], errors: [], skipped: 'unsupported-platform' };
|
|
134
|
+
}
|
|
135
|
+
const moved = [];
|
|
136
|
+
const errors = [];
|
|
137
|
+
const canonical = FIRST_PARTY_HARNESS.provider;
|
|
138
|
+
const root = resolveConfigRoot(env);
|
|
139
|
+
const canonicalHome = firstPartyHarnessHomeDir(env);
|
|
140
|
+
// 1. The home directory — follows the engine's namespace.
|
|
141
|
+
if (fs.lstat(canonicalHome) === null) {
|
|
142
|
+
for (const legacyAppName of FIRST_PARTY_HARNESS_ENGINE.legacyAppNames) {
|
|
143
|
+
const aliasHome = path.join(root, legacyAppName);
|
|
144
|
+
if (!isRealDirectory(fs.lstat(aliasHome)))
|
|
145
|
+
continue;
|
|
146
|
+
try {
|
|
147
|
+
fs.rename(aliasHome, canonicalHome);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
// A concurrent migrator may have won; step 2 still runs against
|
|
151
|
+
// whatever is now at the canonical path.
|
|
152
|
+
errors.push(`could not move ${aliasHome} to ${canonicalHome}: ${describe(err)}`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
moved.push(`${aliasHome} -> ${canonicalHome}`);
|
|
156
|
+
try {
|
|
157
|
+
fs.symlink(path.basename(canonicalHome), aliasHome, 'dir');
|
|
158
|
+
moved.push(`symlink ${aliasHome} -> ${path.basename(canonicalHome)}`);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
errors.push(`moved ${aliasHome} but could not leave a symlink behind: ${describe(err)}`);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 2. The binary and its companions — follow the provider — inside the home.
|
|
167
|
+
const binDir = path.join(canonicalHome, 'bin');
|
|
168
|
+
if (!isRealDirectory(fs.lstat(binDir))) {
|
|
169
|
+
return { moved, errors };
|
|
170
|
+
}
|
|
171
|
+
const canonicalName = firstPartyHarnessBinaryName(canonical, platform);
|
|
172
|
+
const canonicalBinary = path.join(binDir, canonicalName);
|
|
173
|
+
const canonicalStat = fs.lstat(canonicalBinary);
|
|
174
|
+
const aliasNames = FIRST_PARTY_HARNESS.aliases.map((alias) => firstPartyHarnessBinaryName(alias, platform));
|
|
175
|
+
/** Move `<from><suffix>` onto `<to><suffix>` and leave a link; `replaceable` says what may sit at the target. */
|
|
176
|
+
const relocate = (fromName, suffix, replaceable) => {
|
|
177
|
+
const from = path.join(binDir, `${fromName}${suffix}`);
|
|
178
|
+
const to = path.join(binDir, `${canonicalName}${suffix}`);
|
|
179
|
+
const toStat = fs.lstat(to);
|
|
180
|
+
if (!isRealFile(fs.lstat(from)) || !replaceable(to, toStat))
|
|
181
|
+
return 'skipped';
|
|
182
|
+
try {
|
|
183
|
+
// `rename` replaces a LINK at `to` itself, never the file it points at.
|
|
184
|
+
fs.rename(from, to);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
errors.push(`could not move ${from} to ${to}: ${describe(err)}`);
|
|
188
|
+
return 'failed';
|
|
189
|
+
}
|
|
190
|
+
moved.push(`${from} -> ${to}`);
|
|
191
|
+
try {
|
|
192
|
+
fs.symlink(path.basename(to), from, 'file');
|
|
193
|
+
moved.push(`symlink ${from} -> ${path.basename(to)}`);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
errors.push(`moved ${from} but could not leave a symlink behind: ${describe(err)}`);
|
|
197
|
+
}
|
|
198
|
+
return 'moved';
|
|
199
|
+
};
|
|
200
|
+
// 2a. Canonical absent: an ordinary legacy install.
|
|
201
|
+
if (canonicalStat === null) {
|
|
202
|
+
const aliasName = aliasNames.find((name) => isRealFile(fs.lstat(path.join(binDir, name))));
|
|
203
|
+
if (aliasName) {
|
|
204
|
+
for (const suffix of COMPANION_SUFFIXES) {
|
|
205
|
+
const outcome = relocate(aliasName, suffix, (_to, toStat) => toStat === null);
|
|
206
|
+
if (suffix === '' && outcome !== 'moved')
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { moved, errors };
|
|
211
|
+
}
|
|
212
|
+
// 2b. Canonical is a link to a real alias file: adopt the file.
|
|
213
|
+
const linkText = fs.readlink(canonicalBinary);
|
|
214
|
+
if (linkText !== null) {
|
|
215
|
+
const linkedName = path.basename(linkText);
|
|
216
|
+
if (path.dirname(linkText) === '.' &&
|
|
217
|
+
aliasNames.includes(linkedName) &&
|
|
218
|
+
isRealFile(fs.lstat(path.join(binDir, linkedName)))) {
|
|
219
|
+
for (const suffix of COMPANION_SUFFIXES) {
|
|
220
|
+
const outcome = relocate(linkedName, suffix, (to, toStat) => toStat === null || fs.readlink(to) === `${linkedName}${suffix}`);
|
|
221
|
+
if (suffix === '' && outcome !== 'moved')
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { moved, errors };
|
|
226
|
+
}
|
|
227
|
+
// 2c. Both real, alias newer: an update installed under the old name.
|
|
228
|
+
if (isRealFile(canonicalStat)) {
|
|
229
|
+
const newer = aliasNames
|
|
230
|
+
.map((name) => ({ name, stat: fs.lstat(path.join(binDir, name)) }))
|
|
231
|
+
.filter((entry) => isRealFile(entry.stat))
|
|
232
|
+
.filter((entry) => entry.stat.mtimeMs - canonicalStat.mtimeMs > NEWER_THRESHOLD_MS)
|
|
233
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)[0];
|
|
234
|
+
if (newer) {
|
|
235
|
+
const previous = `${canonicalBinary}${FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX}`;
|
|
236
|
+
const previousStat = fs.lstat(previous);
|
|
237
|
+
if (previousStat !== null && !isRealFile(previousStat)) {
|
|
238
|
+
errors.push(`${previous} is not a regular file; left the newer ${newer.name} in place`);
|
|
239
|
+
return { moved, errors };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
// The older canonical engine becomes the rollback copy — the same
|
|
243
|
+
// meaning `.previous` has after any install.
|
|
244
|
+
fs.rename(canonicalBinary, previous);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
errors.push(`could not keep ${canonicalBinary} as ${previous}: ${describe(err)}`);
|
|
248
|
+
return { moved, errors };
|
|
249
|
+
}
|
|
250
|
+
moved.push(`${canonicalBinary} -> ${previous}`);
|
|
251
|
+
for (const suffix of ['', FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX]) {
|
|
252
|
+
// The alias's sidecar describes the alias's bytes, so it replaces the
|
|
253
|
+
// canonical one: a record must always sit beside the binary it describes.
|
|
254
|
+
const outcome = relocate(newer.name, suffix, (to, toStat) => suffix === '' ? toStat === null : toStat === null || isRealFile(toStat));
|
|
255
|
+
if (suffix === '' && outcome !== 'moved')
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { moved, errors };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Leave a relative symlink at every alias name — binary and provenance sidecar —
|
|
264
|
+
* that does not exist yet, pointing at the canonical file.
|
|
265
|
+
*
|
|
266
|
+
* Call after installing the canonical binary. A platform CLI or daemon installed
|
|
267
|
+
* before a rename resolves the legacy name; the link keeps it on the current
|
|
268
|
+
* engine. It never replaces anything that exists, whether a real file (an older
|
|
269
|
+
* engine someone may still run) or a link someone else placed. Never throws.
|
|
270
|
+
*/
|
|
271
|
+
export function linkFirstPartyHarnessAliases(options = {}) {
|
|
272
|
+
const env = options.env ?? process.env;
|
|
273
|
+
const platform = options.platform ?? process.platform;
|
|
274
|
+
const fs = options.fs ?? NODE_FS;
|
|
275
|
+
if (platform === 'win32') {
|
|
276
|
+
return { moved: [], errors: [], skipped: 'unsupported-platform' };
|
|
277
|
+
}
|
|
278
|
+
const moved = [];
|
|
279
|
+
const errors = [];
|
|
280
|
+
const binDir = firstPartyHarnessBinDir(env);
|
|
281
|
+
const canonicalName = firstPartyHarnessBinaryName(FIRST_PARTY_HARNESS.provider, platform);
|
|
282
|
+
if (!isRealFile(fs.lstat(path.join(binDir, canonicalName)))) {
|
|
283
|
+
return { moved, errors };
|
|
284
|
+
}
|
|
285
|
+
for (const alias of FIRST_PARTY_HARNESS.aliases) {
|
|
286
|
+
const aliasName = firstPartyHarnessBinaryName(alias, platform);
|
|
287
|
+
// Sidecar FIRST: a verifier that finds the binary link must find its record.
|
|
288
|
+
for (const suffix of [FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX, '']) {
|
|
289
|
+
const target = `${canonicalName}${suffix}`;
|
|
290
|
+
const at = path.join(binDir, `${aliasName}${suffix}`);
|
|
291
|
+
if (!isRealFile(fs.lstat(path.join(binDir, target))) || fs.lstat(at) !== null)
|
|
292
|
+
continue;
|
|
293
|
+
try {
|
|
294
|
+
fs.symlink(target, at, 'file');
|
|
295
|
+
moved.push(`symlink ${at} -> ${target}`);
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
errors.push(`could not link ${at} -> ${target}: ${describe(err)}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { moved, errors };
|
|
303
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
5
5
|
*
|
|
6
|
-
* `
|
|
6
|
+
* `skrr code doctor` must tell a user what tier their harness is and what that
|
|
7
7
|
* means for credentials and permission prompts. The daemon is the authority for
|
|
8
8
|
* that answer, but the CLI cannot import `daemon/src` — separate packages, and
|
|
9
9
|
* the daemon already depends on the CLI, so the reverse edge would be a cycle.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* parsed the daemon's source to catch drift. That guard worked — and it was still
|
|
19
19
|
* only a guard. The regression it existed to catch had already shipped once: the
|
|
20
20
|
* doctor described Tier 2 behaviour for a day after the owner decision of
|
|
21
|
-
* 2026-08-11 moved
|
|
21
|
+
* 2026-08-11 moved the first-party harness to Tier 1, and a unit test asserting the stale
|
|
22
22
|
* string made the wrong claim look load-bearing.
|
|
23
23
|
*
|
|
24
24
|
* ── What deliberately did NOT move ────────────────────────────────────────
|
|
@@ -44,10 +44,16 @@ export type HarnessTrustTier = 1 | 2 | 3;
|
|
|
44
44
|
* map resolves to, so adding trust is always a deliberate, reviewable edit and
|
|
45
45
|
* never an omission.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
48
|
-
* the measured cost of the Tier 2 posture it replaced, is recorded
|
|
49
|
-
* `daemon/src/harness-trust.ts` beside the gates that read it — the argument
|
|
47
|
+
* The first-party harness is Tier 1 by OWNER DECISION (2026-08-11). The full
|
|
48
|
+
* reasoning, and the measured cost of the Tier 2 posture it replaced, is recorded
|
|
49
|
+
* in `daemon/src/harness-trust.ts` beside the gates that read it — the argument
|
|
50
50
|
* belongs where the enforcement is, not in a shared vocabulary module.
|
|
51
|
+
*
|
|
52
|
+
* It is entered under EVERY spelling (`firstPartyHarnessSpellings`), not only the
|
|
53
|
+
* canonical one. A backend name reaches this lookup from a daemon of any version,
|
|
54
|
+
* and an alias that fell through to the Tier 3 default would silently strip the
|
|
55
|
+
* harness of the trust the owner granted it — a failure that looks like a
|
|
56
|
+
* permission prompt storm, not like a rename.
|
|
51
57
|
*/
|
|
52
58
|
export declare const HARNESS_TIERS: Readonly<Record<string, HarnessTrustTier>>;
|
|
53
59
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
5
5
|
*
|
|
6
|
-
* `
|
|
6
|
+
* `skrr code doctor` must tell a user what tier their harness is and what that
|
|
7
7
|
* means for credentials and permission prompts. The daemon is the authority for
|
|
8
8
|
* that answer, but the CLI cannot import `daemon/src` — separate packages, and
|
|
9
9
|
* the daemon already depends on the CLI, so the reverse edge would be a cycle.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* parsed the daemon's source to catch drift. That guard worked — and it was still
|
|
19
19
|
* only a guard. The regression it existed to catch had already shipped once: the
|
|
20
20
|
* doctor described Tier 2 behaviour for a day after the owner decision of
|
|
21
|
-
* 2026-08-11 moved
|
|
21
|
+
* 2026-08-11 moved the first-party harness to Tier 1, and a unit test asserting the stale
|
|
22
22
|
* string made the wrong claim look load-bearing.
|
|
23
23
|
*
|
|
24
24
|
* ── What deliberately did NOT move ────────────────────────────────────────
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* failure than the drift this file fixes: a confident wrong answer beats an
|
|
35
35
|
* honestly stale one for causing harm.
|
|
36
36
|
*/
|
|
37
|
+
import { firstPartyHarnessSpellings } from './firstPartyHarness.js';
|
|
37
38
|
/**
|
|
38
39
|
* Trust tier by backend name.
|
|
39
40
|
*
|
|
@@ -42,20 +43,26 @@
|
|
|
42
43
|
* map resolves to, so adding trust is always a deliberate, reviewable edit and
|
|
43
44
|
* never an omission.
|
|
44
45
|
*
|
|
45
|
-
*
|
|
46
|
-
* the measured cost of the Tier 2 posture it replaced, is recorded
|
|
47
|
-
* `daemon/src/harness-trust.ts` beside the gates that read it — the argument
|
|
46
|
+
* The first-party harness is Tier 1 by OWNER DECISION (2026-08-11). The full
|
|
47
|
+
* reasoning, and the measured cost of the Tier 2 posture it replaced, is recorded
|
|
48
|
+
* in `daemon/src/harness-trust.ts` beside the gates that read it — the argument
|
|
48
49
|
* belongs where the enforcement is, not in a shared vocabulary module.
|
|
50
|
+
*
|
|
51
|
+
* It is entered under EVERY spelling (`firstPartyHarnessSpellings`), not only the
|
|
52
|
+
* canonical one. A backend name reaches this lookup from a daemon of any version,
|
|
53
|
+
* and an alias that fell through to the Tier 3 default would silently strip the
|
|
54
|
+
* harness of the trust the owner granted it — a failure that looks like a
|
|
55
|
+
* permission prompt storm, not like a rename.
|
|
49
56
|
*/
|
|
50
57
|
export const HARNESS_TIERS = Object.freeze({
|
|
51
|
-
// --- Tier 1: first-party / audited ---
|
|
58
|
+
// --- Tier 1: vendor first-party / audited ---
|
|
52
59
|
claude: 1,
|
|
53
60
|
codex: 1,
|
|
54
61
|
opencode: 1,
|
|
55
62
|
'opencode-acp': 1,
|
|
56
63
|
gemini: 1,
|
|
57
64
|
'gemini-acp': 1,
|
|
58
|
-
|
|
65
|
+
...Object.fromEntries(firstPartyHarnessSpellings().map((spelling) => [spelling, 1])),
|
|
59
66
|
// --- Tier 2: reviewed open-source community harnesses ---
|
|
60
67
|
goose: 2,
|
|
61
68
|
aider: 2,
|
|
@@ -35,7 +35,9 @@ export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConf
|
|
|
35
35
|
export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
|
|
36
36
|
export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
|
|
37
37
|
export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
|
|
38
|
-
export
|
|
38
|
+
export * from './firstPartyHarness.js';
|
|
39
|
+
export { FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX, FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX, firstPartyHarnessBinDir, firstPartyHarnessHomeDir, firstPartyHarnessInstalledBinaryPath, linkFirstPartyHarnessAliases, migrateFirstPartyHarnessHome, type FirstPartyHarnessHomeFs, type FirstPartyHarnessHomeMigration, } from './firstPartyHarnessHome.js';
|
|
40
|
+
export { DEFAULT_FIRST_PARTY_HARNESS_CHANNEL, FIRST_PARTY_HARNESS_CHANNELS, firstPartyHarnessChannelPrefix, firstPartyHarnessFeedBase, firstPartyHarnessFeedCandidates, isFirstPartyHarnessChannel, resolveChannelManifestUrl, resolveFirstPartyHarnessChannel, type FirstPartyHarnessChannel, type FirstPartyHarnessFeedCandidate, DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, type SkyCodeChannel, } from './firstPartyHarnessChannels.js';
|
|
39
41
|
export { OVERSKY_RELEASE_PUBLIC_KEYS, type ReleaseKey } from './releaseKeys.js';
|
|
40
42
|
export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
|
|
41
43
|
export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
|
|
@@ -126,7 +126,7 @@ export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest,
|
|
|
126
126
|
// shapes locally because it can't pull a Node-shaped package.
|
|
127
127
|
export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, } from './daemonPair.js';
|
|
128
128
|
// OSK-3897 — harness trust tiers, shared by the daemon (the authority) and the
|
|
129
|
-
// CLI (which publishes the fact in `
|
|
129
|
+
// CLI (which publishes the fact in `skrr code doctor`). The PROVENANCE half stays
|
|
130
130
|
// in the daemon on purpose: a tier is a published fact, provenance is evidence
|
|
131
131
|
// about bytes on disk that only the spawning process can gather.
|
|
132
132
|
export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, } from './harnessTrust.js';
|
|
@@ -142,7 +142,17 @@ export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATI
|
|
|
142
142
|
/* Pre-rename state detection. Callers REFUSE on a positive result; nothing here
|
|
143
143
|
reads, copies or removes what it finds (§7.2, §8.4). */
|
|
144
144
|
export { findLegacyLocalState, describeLegacyState, } from './legacyStatePreflight.js';
|
|
145
|
-
|
|
145
|
+
// The first-party harness identity: the ONE place its product name is written.
|
|
146
|
+
// Pure, and also published as the `./first-party-harness` subpath for browser and
|
|
147
|
+
// React Native consumers. See firstPartyHarness.ts.
|
|
148
|
+
export * from './firstPartyHarness.js';
|
|
149
|
+
// Where the first-party harness lives under the skrr root, and the one-time move
|
|
150
|
+
// that keeps legacy installs working across a rename. Node-only.
|
|
151
|
+
export { FIRST_PARTY_HARNESS_PREVIOUS_SUFFIX, FIRST_PARTY_HARNESS_PROVENANCE_SUFFIX, firstPartyHarnessBinDir, firstPartyHarnessHomeDir, firstPartyHarnessInstalledBinaryPath, linkFirstPartyHarnessAliases, migrateFirstPartyHarnessHome, } from './firstPartyHarnessHome.js';
|
|
152
|
+
export { DEFAULT_FIRST_PARTY_HARNESS_CHANNEL, FIRST_PARTY_HARNESS_CHANNELS, firstPartyHarnessChannelPrefix, firstPartyHarnessFeedBase, firstPartyHarnessFeedCandidates, isFirstPartyHarnessChannel, resolveChannelManifestUrl, resolveFirstPartyHarnessChannel,
|
|
153
|
+
// Deprecated names kept for installed CLIs that resolve this published package
|
|
154
|
+
// with a caret range. Removed in the contract phase with a minor bump.
|
|
155
|
+
DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, } from './firstPartyHarnessChannels.js';
|
|
146
156
|
// Daemon release trust: the pinned ed25519 keys and the canonical signature
|
|
147
157
|
// messages. Shared because BOTH the daemon (self-update) and the CLI
|
|
148
158
|
// (`skrr daemon install`, which runs when there is no daemon to ask) verify the
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* WHY IT LIVES IN auth-core NOW
|
|
20
20
|
*
|
|
21
|
-
* It lived in `daemon/src/release-manifest.ts`, and `
|
|
21
|
+
* It lived in `daemon/src/release-manifest.ts`, and `firstPartyHarnessChannels.ts` beside
|
|
22
22
|
* it recorded the reason for stopping there: "the installer, the signature
|
|
23
23
|
* verification and the promotion machinery stay in the daemon, which is the
|
|
24
24
|
* only process that performs them." That premise held exactly until the CLI
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
6
6
|
*
|
|
7
7
|
* The daemon has stripped its own credentials out of `process.env` before every
|
|
8
|
-
* harness spawn since `daemon/src/env-sanitize.ts` was written. `
|
|
9
|
-
* THE SAME engine — `daemon/src/backends/acp.ts` and `cli/src/lib/
|
|
8
|
+
* harness spawn since `daemon/src/env-sanitize.ts` was written. `skrr code` spawns
|
|
9
|
+
* THE SAME engine — `daemon/src/backends/acp.ts` and `cli/src/lib/first-party-harness.ts`
|
|
10
10
|
* resolve the same binary, from the same managed location — and had no
|
|
11
11
|
* equivalent, so `OVERSKY_TOKEN` (a cloud-reusable platform credential that
|
|
12
12
|
* authenticates every `/api/*` call as the human) entered the environment of a
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
* This is a DENY-LIST over the parent's own environment, and it strips provider
|
|
33
33
|
* credentials because the daemon delivers a child's model credential explicitly
|
|
34
34
|
* (`cred.env`, after the trust gate) and never by ambient inheritance. A parent
|
|
35
|
-
* whose supported mode IS ambient inheritance — `
|
|
35
|
+
* whose supported mode IS ambient inheritance — `skrr code` running BYOK, where
|
|
36
36
|
* the user's own key is the credential the run is meant to spend — has to
|
|
37
37
|
* re-admit that credential deliberately after this filter. See
|
|
38
|
-
* `cli/src/lib/
|
|
38
|
+
* `cli/src/lib/first-party-harness.ts` (`engineSpawnEnv`). Do not soften the list here to
|
|
39
39
|
* serve that case: a deny-list with a hole is not a deny-list.
|
|
40
40
|
*/
|
|
41
41
|
/**
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
6
6
|
*
|
|
7
7
|
* The daemon has stripped its own credentials out of `process.env` before every
|
|
8
|
-
* harness spawn since `daemon/src/env-sanitize.ts` was written. `
|
|
9
|
-
* THE SAME engine — `daemon/src/backends/acp.ts` and `cli/src/lib/
|
|
8
|
+
* harness spawn since `daemon/src/env-sanitize.ts` was written. `skrr code` spawns
|
|
9
|
+
* THE SAME engine — `daemon/src/backends/acp.ts` and `cli/src/lib/first-party-harness.ts`
|
|
10
10
|
* resolve the same binary, from the same managed location — and had no
|
|
11
11
|
* equivalent, so `OVERSKY_TOKEN` (a cloud-reusable platform credential that
|
|
12
12
|
* authenticates every `/api/*` call as the human) entered the environment of a
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
* This is a DENY-LIST over the parent's own environment, and it strips provider
|
|
33
33
|
* credentials because the daemon delivers a child's model credential explicitly
|
|
34
34
|
* (`cred.env`, after the trust gate) and never by ambient inheritance. A parent
|
|
35
|
-
* whose supported mode IS ambient inheritance — `
|
|
35
|
+
* whose supported mode IS ambient inheritance — `skrr code` running BYOK, where
|
|
36
36
|
* the user's own key is the credential the run is meant to spend — has to
|
|
37
37
|
* re-admit that credential deliberately after this filter. See
|
|
38
|
-
* `cli/src/lib/
|
|
38
|
+
* `cli/src/lib/first-party-harness.ts` (`engineSpawnEnv`). Do not soften the list here to
|
|
39
39
|
* serve that case: a deny-list with a hole is not a deny-list.
|
|
40
40
|
*/
|
|
41
41
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skrr-ai/auth-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"types": "dist/esm/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
"types": "./dist/cjs/refreshClassification.d.ts",
|
|
24
24
|
"default": "./dist/cjs/refreshClassification.js"
|
|
25
25
|
}
|
|
26
|
+
},
|
|
27
|
+
"./first-party-harness": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/esm/firstPartyHarness.d.ts",
|
|
30
|
+
"default": "./dist/esm/firstPartyHarness.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/cjs/firstPartyHarness.d.ts",
|
|
34
|
+
"default": "./dist/cjs/firstPartyHarness.js"
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
},
|
|
28
38
|
"dependencies": {
|