@skrr-ai/cli 0.1.50 → 0.1.52
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/dist/base-command.d.ts +7 -0
- package/dist/base-command.js +95 -8
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +8 -4
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +32 -0
- package/dist/commands/agents/update.js +81 -9
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- package/dist/commands/commitments/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +15 -1
- package/dist/commands/commitments/next-tick.js +12 -2
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/context.js +2 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +53 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/doctor.js +8 -1
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +26 -7
- package/dist/commands/logout.js +3 -0
- package/dist/commands/machines/dedicated/show.js +25 -2
- package/dist/commands/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +6 -0
- package/dist/commands/tasks/create.js +31 -6
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/start.d.ts +14 -0
- package/dist/commands/tasks/start.js +31 -1
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +29 -1
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/commands/whoami.js +5 -3
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +14 -10
- package/dist/lib/agentic-stream.d.ts +88 -1
- package/dist/lib/agentic-stream.js +330 -9
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +68 -0
- package/dist/lib/auth-storage.d.ts +24 -0
- package/dist/lib/auth-storage.js +342 -11
- package/dist/lib/cli-id.d.ts +22 -0
- package/dist/lib/cli-id.js +56 -0
- package/dist/lib/cli-identity.d.ts +49 -0
- package/dist/lib/cli-identity.js +159 -0
- package/dist/lib/commitments.d.ts +25 -1
- package/dist/lib/commitments.js +46 -1
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +15 -3
- package/dist/lib/daemonBroker.d.ts +37 -1
- package/dist/lib/daemonBroker.js +146 -22
- package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
- package/dist/lib/daemonBrokerRefusal.js +127 -0
- package/dist/lib/dedicated-machines.d.ts +33 -2
- package/dist/lib/dedicated-machines.js +50 -1
- package/dist/lib/delegated-cli.js +20 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.js +10 -5
- package/dist/lib/harnesses.d.ts +10 -0
- package/dist/lib/keychain.d.ts +39 -0
- package/dist/lib/keychain.js +228 -39
- package/dist/lib/label-ref.d.ts +34 -0
- package/dist/lib/label-ref.js +68 -1
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +67 -21
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/oauthLogin.js +2 -8
- package/dist/lib/refresh.js +6 -2
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +87 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- 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 +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- 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 +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3139 -3097
- package/oclif.manifest.json +28591 -28438
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.profileFromArgs = profileFromArgs;
|
|
7
|
+
exports.decideStatusBinary = decideStatusBinary;
|
|
8
|
+
exports.describeStatusBinary = describeStatusBinary;
|
|
9
|
+
exports.scriptFromArgs = scriptFromArgs;
|
|
10
|
+
exports.defaultLiveRuntimeDeps = defaultLiveRuntimeDeps;
|
|
11
|
+
/**
|
|
12
|
+
* Which runtime binary is the LIVE daemon running, and is it the one
|
|
13
|
+
* `skrr daemon status` is about to forward to? (OSK-12102)
|
|
14
|
+
*
|
|
15
|
+
* `execRuntimeBinary` forwards to the first `skrrd` on PATH. That is right for
|
|
16
|
+
* lifecycle verbs, where PATH order is the person's choice of install. It is
|
|
17
|
+
* wrong for `status`, whose whole subject is the daemon that is running: with
|
|
18
|
+
* an old dev build early on PATH and the release daemon live, the header named
|
|
19
|
+
* `.../releases/0.8.62/skrrd` while the body was rendered by the dev build —
|
|
20
|
+
* its old layout, no per-category permission table — and nothing on screen
|
|
21
|
+
* said two binaries were involved.
|
|
22
|
+
*
|
|
23
|
+
* The live daemon's pid comes from its island bootstrap file (the same
|
|
24
|
+
* discovery the CLI's login broker uses); the program from the OS. When the
|
|
25
|
+
* live program is itself a runtime executable, status is forwarded to it.
|
|
26
|
+
* When it cannot be exec'd directly (a `node .../dist/index.js` dev daemon),
|
|
27
|
+
* status stays on the PATH binary and says so.
|
|
28
|
+
*/
|
|
29
|
+
const node_child_process_1 = require("node:child_process");
|
|
30
|
+
const node_fs_1 = require("node:fs");
|
|
31
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
32
|
+
const daemonBroker_1 = require("./daemonBroker");
|
|
33
|
+
const exec_runtime_binary_1 = require("./exec-runtime-binary");
|
|
34
|
+
function safeRealpath(deps, p) {
|
|
35
|
+
try {
|
|
36
|
+
return deps.realpath(p);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isRuntimeName(p) {
|
|
43
|
+
const base = node_path_1.default
|
|
44
|
+
.basename(p)
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/\.(exe|cmd)$/, '');
|
|
47
|
+
return exec_runtime_binary_1.RUNTIME_BINARY_NAMES.some((name) => base === name || base.startsWith(`${name}-`));
|
|
48
|
+
}
|
|
49
|
+
/** The profile a forwarded argv names, `default` otherwise. */
|
|
50
|
+
function profileFromArgs(args) {
|
|
51
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg === '--')
|
|
54
|
+
break;
|
|
55
|
+
if (arg === '--profile' && args[i + 1] && !args[i + 1].startsWith('-'))
|
|
56
|
+
return args[i + 1];
|
|
57
|
+
if (arg.startsWith('--profile=') && arg.length > '--profile='.length) {
|
|
58
|
+
return arg.slice('--profile='.length);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return 'default';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Decide which binary renders `status`. Never throws: a probe that cannot
|
|
65
|
+
* answer leaves the PATH binary in place (`unknown`).
|
|
66
|
+
*/
|
|
67
|
+
function decideStatusBinary(pathBinary, profile, deps) {
|
|
68
|
+
let program = null;
|
|
69
|
+
try {
|
|
70
|
+
const pid = deps.livePid(profile);
|
|
71
|
+
program = pid ? deps.programOf(pid) : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
program = null;
|
|
75
|
+
}
|
|
76
|
+
if (!program)
|
|
77
|
+
return { kind: 'unknown', binary: pathBinary };
|
|
78
|
+
const pathReal = safeRealpath(deps, pathBinary);
|
|
79
|
+
const execReal = safeRealpath(deps, program.exec);
|
|
80
|
+
const scriptReal = program.script ? safeRealpath(deps, program.script) : null;
|
|
81
|
+
if (pathReal === execReal || (scriptReal !== null && pathReal === scriptReal)) {
|
|
82
|
+
return { kind: 'same', binary: pathBinary };
|
|
83
|
+
}
|
|
84
|
+
// Only a runtime executable is forwarded to. A `node`/Electron-hosted daemon
|
|
85
|
+
// has an interpreter as its exec, and running the interpreter with status
|
|
86
|
+
// argv would be nonsense.
|
|
87
|
+
const directlyRunnable = program.script === null &&
|
|
88
|
+
(isRuntimeName(program.exec) || isRuntimeName(execReal)) &&
|
|
89
|
+
deps.isExecutable(program.exec);
|
|
90
|
+
if (directlyRunnable) {
|
|
91
|
+
return { kind: 'forward-live', binary: program.exec, pathBinary };
|
|
92
|
+
}
|
|
93
|
+
return { kind: 'mismatch', binary: pathBinary, live: program.script ?? program.exec };
|
|
94
|
+
}
|
|
95
|
+
/** One stderr line naming the binary that rendered status, when it is not obvious. */
|
|
96
|
+
function describeStatusBinary(decision) {
|
|
97
|
+
switch (decision.kind) {
|
|
98
|
+
case 'forward-live':
|
|
99
|
+
return (`Rendered by the live daemon's binary ${decision.binary} ` +
|
|
100
|
+
`(the first skrrd on PATH is ${decision.pathBinary}, a different build).`);
|
|
101
|
+
case 'mismatch':
|
|
102
|
+
return (`Rendered by ${decision.binary}, the first skrrd on PATH — NOT the live daemon's ` +
|
|
103
|
+
`program (${decision.live}). This layout and these checks are that build's, not the live one's.`);
|
|
104
|
+
default:
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function darwinProgramOf(pid) {
|
|
109
|
+
const out = (args) => (0, node_child_process_1.execFileSync)('ps', ['-p', String(pid), '-o', ...args], {
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
timeout: 2_000,
|
|
112
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
113
|
+
}).trim();
|
|
114
|
+
const exec = out(['comm=']);
|
|
115
|
+
if (!exec || !node_path_1.default.isAbsolute(exec))
|
|
116
|
+
return null;
|
|
117
|
+
return { exec, script: scriptFromArgs(exec, out(['args='])) };
|
|
118
|
+
}
|
|
119
|
+
function linuxProgramOf(pid) {
|
|
120
|
+
const exec = (0, node_fs_1.readlinkSync)(`/proc/${pid}/exe`);
|
|
121
|
+
let script = null;
|
|
122
|
+
try {
|
|
123
|
+
const cmdline = (0, node_fs_1.readFileSync)(`/proc/${pid}/cmdline`, 'utf8');
|
|
124
|
+
script = scriptFromArgs(exec, cmdline.split('\0').filter(Boolean).join(' '));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
script = null;
|
|
128
|
+
}
|
|
129
|
+
return { exec, script };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* For an interpreter-hosted daemon (`node --import x.mjs /x/dist/index.js
|
|
133
|
+
* start …`), the script: the first argument after the exec that is neither an
|
|
134
|
+
* option nor the value of a module-loading option. `ps -o args=` is
|
|
135
|
+
* space-joined, so a path containing a space is not recovered — the result is
|
|
136
|
+
* only ever used for display and an equality check, where a miss degrades to
|
|
137
|
+
* the "mismatch" note rather than to a wrong forward.
|
|
138
|
+
*/
|
|
139
|
+
const INTERPRETER_VALUE_OPTIONS = new Set([
|
|
140
|
+
'-r',
|
|
141
|
+
'--require',
|
|
142
|
+
'--import',
|
|
143
|
+
'--loader',
|
|
144
|
+
'--experimental-loader',
|
|
145
|
+
'-e',
|
|
146
|
+
'--eval',
|
|
147
|
+
]);
|
|
148
|
+
function scriptFromArgs(exec, args) {
|
|
149
|
+
if (isRuntimeName(exec))
|
|
150
|
+
return null;
|
|
151
|
+
const rest = args.startsWith(exec) ? args.slice(exec.length) : args.replace(/^\S+/, '');
|
|
152
|
+
const tokens = rest.split(/\s+/).filter(Boolean);
|
|
153
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
154
|
+
const token = tokens[i];
|
|
155
|
+
if (INTERPRETER_VALUE_OPTIONS.has(token)) {
|
|
156
|
+
i += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (token.startsWith('-'))
|
|
160
|
+
continue;
|
|
161
|
+
return token;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function defaultLiveRuntimeDeps() {
|
|
166
|
+
return {
|
|
167
|
+
livePid(profile) {
|
|
168
|
+
for (const candidate of (0, daemonBroker_1.resolveExactProfileBootstrapPaths)(profile)) {
|
|
169
|
+
const bootstrap = (0, daemonBroker_1.readBootstrap)(candidate);
|
|
170
|
+
if (typeof bootstrap?.pid === 'number' && bootstrap.pid > 0)
|
|
171
|
+
return bootstrap.pid;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
},
|
|
175
|
+
programOf(pid) {
|
|
176
|
+
if (process.platform === 'darwin')
|
|
177
|
+
return darwinProgramOf(pid);
|
|
178
|
+
if (process.platform === 'linux')
|
|
179
|
+
return linuxProgramOf(pid);
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
realpath: (p) => (0, node_fs_1.realpathSync)(p),
|
|
183
|
+
isExecutable(p) {
|
|
184
|
+
try {
|
|
185
|
+
(0, node_fs_1.accessSync)(p, node_fs_1.constants.X_OK);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
package/dist/lib/login.d.ts
CHANGED
|
@@ -38,6 +38,29 @@ export interface CliLoginWithRefreshTokenOptions {
|
|
|
38
38
|
*/
|
|
39
39
|
cliId: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* What a shell with no terminal can do instead (OSK-12117). The refusal used to
|
|
43
|
+
* name only CI tokens, which is not a sign-in: an agent session asking for one
|
|
44
|
+
* could only hand the job to a person typing `! skrr login`.
|
|
45
|
+
*/
|
|
46
|
+
export declare const HEADLESS_LOGIN_HINT: string;
|
|
47
|
+
/**
|
|
48
|
+
* The line printed when the daemon descriptor this CLI found is for another
|
|
49
|
+
* server (OSK-12117).
|
|
50
|
+
*
|
|
51
|
+
* It used to assert "the skrr background service on this computer is logged in
|
|
52
|
+
* to <url>" and recommend repointing the CLI there. What the CLI actually knows
|
|
53
|
+
* is narrower: ONE descriptor file it read names that server. That file can
|
|
54
|
+
* belong to another profile's daemon (observed: a localhost:3080 descriptor
|
|
55
|
+
* while this machine's default daemon was connected to skrr.ai), and in that
|
|
56
|
+
* case both the claim and the advice were wrong — the advice by moving a
|
|
57
|
+
* production CLI onto a development server. So say what was read, from where,
|
|
58
|
+
* and how to check the daemon itself.
|
|
59
|
+
*/
|
|
60
|
+
export declare function describeBrokerBaseUrlMismatch(failure: {
|
|
61
|
+
daemonServerUrl?: string;
|
|
62
|
+
descriptorPath?: string;
|
|
63
|
+
}, serverUrl: string): string;
|
|
41
64
|
/**
|
|
42
65
|
* Top-level dispatcher. Picks PKCE localhost vs device-code per the
|
|
43
66
|
* decision tree in the roadmap and persists the result.
|
package/dist/lib/login.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HEADLESS_LOGIN_HINT = void 0;
|
|
4
|
+
exports.describeBrokerBaseUrlMismatch = describeBrokerBaseUrlMismatch;
|
|
3
5
|
exports.cliLogin = cliLogin;
|
|
4
6
|
exports.cliLoginWithRefreshToken = cliLoginWithRefreshToken;
|
|
5
7
|
/**
|
|
@@ -29,12 +31,40 @@ const node_child_process_1 = require("node:child_process");
|
|
|
29
31
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
30
32
|
const auth_storage_1 = require("./auth-storage");
|
|
31
33
|
const config_1 = require("./config");
|
|
32
|
-
const
|
|
34
|
+
const cli_identity_1 = require("./cli-identity");
|
|
33
35
|
const ssh_detect_1 = require("./ssh-detect");
|
|
34
36
|
const device_code_1 = require("./device-code");
|
|
35
37
|
const device_identity_1 = require("./device-identity");
|
|
36
38
|
const oauthLogin_1 = require("./oauthLogin");
|
|
37
39
|
const daemonBroker_1 = require("./daemonBroker");
|
|
40
|
+
const daemonBrokerRefusal_1 = require("./daemonBrokerRefusal");
|
|
41
|
+
/**
|
|
42
|
+
* What a shell with no terminal can do instead (OSK-12117). The refusal used to
|
|
43
|
+
* name only CI tokens, which is not a sign-in: an agent session asking for one
|
|
44
|
+
* could only hand the job to a person typing `! skrr login`.
|
|
45
|
+
*/
|
|
46
|
+
exports.HEADLESS_LOGIN_HINT = '`skrr login --device-code` works without a terminal: it prints a URL and a code for a ' +
|
|
47
|
+
'person to approve in any browser, and finishes when they do. For CI, use ' +
|
|
48
|
+
'`skrr create-token` and set OVERSKY_TOKEN=osk_ci_...';
|
|
49
|
+
/**
|
|
50
|
+
* The line printed when the daemon descriptor this CLI found is for another
|
|
51
|
+
* server (OSK-12117).
|
|
52
|
+
*
|
|
53
|
+
* It used to assert "the skrr background service on this computer is logged in
|
|
54
|
+
* to <url>" and recommend repointing the CLI there. What the CLI actually knows
|
|
55
|
+
* is narrower: ONE descriptor file it read names that server. That file can
|
|
56
|
+
* belong to another profile's daemon (observed: a localhost:3080 descriptor
|
|
57
|
+
* while this machine's default daemon was connected to skrr.ai), and in that
|
|
58
|
+
* case both the claim and the advice were wrong — the advice by moving a
|
|
59
|
+
* production CLI onto a development server. So say what was read, from where,
|
|
60
|
+
* and how to check the daemon itself.
|
|
61
|
+
*/
|
|
62
|
+
function describeBrokerBaseUrlMismatch(failure, serverUrl) {
|
|
63
|
+
const where = failure.descriptorPath ? ` (${failure.descriptorPath})` : '';
|
|
64
|
+
return (` Skipped the daemon hand-off: the daemon descriptor found${where} is for ` +
|
|
65
|
+
`${failure.daemonServerUrl}, not ${serverUrl}. It may belong to a different profile's ` +
|
|
66
|
+
'daemon; `skrr daemon status` shows which server this profile’s daemon is on.');
|
|
67
|
+
}
|
|
38
68
|
/**
|
|
39
69
|
* Top-level dispatcher. Picks PKCE localhost vs device-code per the
|
|
40
70
|
* decision tree in the roadmap and persists the result.
|
|
@@ -44,16 +74,16 @@ async function cliLogin(opts = {}) {
|
|
|
44
74
|
const serverUrl = (opts.serverUrl ?? cfg.baseURL).replace(/\/+$/, '');
|
|
45
75
|
const scope = opts.scope ?? 'cli';
|
|
46
76
|
// Persist the cliId on first login so future refreshes present a
|
|
47
|
-
// stable device identity.
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
(0, config_1.saveConfig)({ ...cfg, cliId });
|
|
52
|
-
}
|
|
77
|
+
// stable device identity. A named profile signing in with no credential is
|
|
78
|
+
// issued its OWN id here, so the hand-off confirm below cannot retire another
|
|
79
|
+
// profile's session (OSK-12156); see `cli-identity.ts`.
|
|
80
|
+
const cliId = (0, cli_identity_1.cliIdForLogin)(cfg);
|
|
53
81
|
const envForce = isTruthy(process.env.OVERSKY_FORCE_DEVICE_CODE);
|
|
54
82
|
const ssh = (0, ssh_detect_1.detectSshSession)();
|
|
55
83
|
/** Set when the broker was refused for a reason that WAITING fixes. */
|
|
56
84
|
let brokerRateLimited = false;
|
|
85
|
+
/** Set when a live daemon answered the hand-off and refused it (OSK-12149). */
|
|
86
|
+
let brokerRefusal = null;
|
|
57
87
|
// --- Daemon-as-broker pre-flight ---------------------------------------
|
|
58
88
|
// When the local `oversky` daemon is already authenticated for this user
|
|
59
89
|
// on this machine, the cleanest UX is no second browser flow at all:
|
|
@@ -100,7 +130,8 @@ async function cliLogin(opts = {}) {
|
|
|
100
130
|
};
|
|
101
131
|
}
|
|
102
132
|
console.log('');
|
|
103
|
-
console.log(' Authorized via the skrr background service on this computer (no browser needed).'
|
|
133
|
+
console.log(' Authorized via the skrr background service on this computer (no browser needed). ' +
|
|
134
|
+
'Stored a durable session for this profile.');
|
|
104
135
|
return {
|
|
105
136
|
token: brokerResult.accessToken,
|
|
106
137
|
refreshToken: brokerResult.refreshToken,
|
|
@@ -148,25 +179,42 @@ async function cliLogin(opts = {}) {
|
|
|
148
179
|
}
|
|
149
180
|
if (brokerResult.reason === 'base_url_mismatch' && brokerResult.daemonServerUrl) {
|
|
150
181
|
console.log('');
|
|
151
|
-
console.log(
|
|
152
|
-
|
|
182
|
+
console.log(describeBrokerBaseUrlMismatch(brokerResult, serverUrl));
|
|
183
|
+
}
|
|
184
|
+
// The daemon answered and refused. Say so before whatever comes next —
|
|
185
|
+
// a browser that opens with no stated cause, or a refusal about terminals
|
|
186
|
+
// that says nothing about the daemon, both hid the real reason.
|
|
187
|
+
brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(brokerResult);
|
|
188
|
+
if (brokerRefusal && !(0, ssh_detect_1.isNonInteractive)()) {
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(` Asked the local skrr service for a session first. ${brokerRefusal.headline}`);
|
|
191
|
+
console.log(' Continuing with a sign-in of this CLI’s own.');
|
|
153
192
|
}
|
|
154
|
-
}
|
|
155
|
-
if ((0, ssh_detect_1.isNonInteractive)()) {
|
|
156
|
-
throw new Error(brokerRateLimited
|
|
157
|
-
? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
|
|
158
|
-
'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
|
|
159
|
-
'retry shortly. For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...'
|
|
160
|
-
: 'skrr login requires an interactive terminal or an SSH session. ' +
|
|
161
|
-
'For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...');
|
|
162
193
|
}
|
|
163
194
|
// --- Decision tree (first match wins) ---
|
|
195
|
+
//
|
|
196
|
+
// Device code BEFORE the terminal check (OSK-12117). It needs no terminal of
|
|
197
|
+
// its own: it prints a URL and a code, a person approves them in any browser,
|
|
198
|
+
// and this process polls until they do. That is the one way an agent session
|
|
199
|
+
// or any other shell without a TTY can finish a login, and the refusal below
|
|
200
|
+
// used to fire first — so `--device-code` was refused for lacking the very
|
|
201
|
+
// thing it does not use.
|
|
164
202
|
if (opts.forceDeviceCode) {
|
|
165
203
|
return runDeviceCode(serverUrl, cliId, 'user_requested', ssh);
|
|
166
204
|
}
|
|
167
205
|
if (envForce) {
|
|
168
206
|
return runDeviceCode(serverUrl, cliId, 'env_force', ssh);
|
|
169
207
|
}
|
|
208
|
+
if ((0, ssh_detect_1.isNonInteractive)()) {
|
|
209
|
+
throw new Error(brokerRateLimited
|
|
210
|
+
? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
|
|
211
|
+
'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
|
|
212
|
+
`retry shortly, or sign in now with ${exports.HEADLESS_LOGIN_HINT}`
|
|
213
|
+
: brokerRefusal
|
|
214
|
+
? `${brokerRefusal.headline} ${brokerRefusal.remedy}\n` +
|
|
215
|
+
`skrr login would otherwise open a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`
|
|
216
|
+
: `skrr login opens a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`);
|
|
217
|
+
}
|
|
170
218
|
if (ssh.detected) {
|
|
171
219
|
// Device-code's "open this URL on your laptop" UX is the correct
|
|
172
220
|
// move under SSH — the localhost callback would bind on the remote
|
|
@@ -248,9 +296,7 @@ async function cliLoginWithRefreshToken(opts) {
|
|
|
248
296
|
// the operator retries. The refresh exchange itself is idempotent on
|
|
249
297
|
// the server side (rotation is the unit of retry); writing the cliId
|
|
250
298
|
// up-front never causes a server-state mismatch.
|
|
251
|
-
|
|
252
|
-
(0, config_1.saveConfig)({ ...cfg, cliId: opts.cliId });
|
|
253
|
-
}
|
|
299
|
+
(0, cli_identity_1.persistPresentedCliId)(cfg, opts.cliId);
|
|
254
300
|
let pair;
|
|
255
301
|
try {
|
|
256
302
|
pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, opts.refreshToken, opts.cliId, {
|
|
@@ -11,6 +11,7 @@ exports.canonicalSlackChannelRequest = canonicalSlackChannelRequest;
|
|
|
11
11
|
exports.canonicalSlackOwnerDmRequest = canonicalSlackOwnerDmRequest;
|
|
12
12
|
exports.rejectAutonomousSlackTarget = rejectAutonomousSlackTarget;
|
|
13
13
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
14
|
+
const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
|
|
14
15
|
const api_fetch_1 = require("./api-fetch");
|
|
15
16
|
const daemonBroker_1 = require("./daemonBroker");
|
|
16
17
|
const runtime_context_1 = require("./runtime-context");
|
|
@@ -152,7 +153,8 @@ async function submitDaemonLoopbackMessageIntent({ context, request, env, bootst
|
|
|
152
153
|
});
|
|
153
154
|
try {
|
|
154
155
|
const response = await Promise.race([
|
|
155
|
-
|
|
156
|
+
// The daemon secret and session token stay on loopback (OSK-12134).
|
|
157
|
+
(fetchImpl || loopback_http_1.loopbackFetch)(`http://${bootstrap.host}:${bootstrap.port}/v1/agent/message-intents`, {
|
|
156
158
|
method: 'POST',
|
|
157
159
|
headers: {
|
|
158
160
|
Authorization: `Bearer ${bootstrap.secret}`,
|
package/dist/lib/oauthLogin.js
CHANGED
|
@@ -44,7 +44,7 @@ const node_http_1 = __importDefault(require("node:http"));
|
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
45
|
const auth_storage_1 = require("./auth-storage");
|
|
46
46
|
const config_1 = require("./config");
|
|
47
|
-
const
|
|
47
|
+
const cli_identity_1 = require("./cli-identity");
|
|
48
48
|
const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
50
|
// PKCE helpers
|
|
@@ -272,13 +272,7 @@ async function runOAuthLogin(opts) {
|
|
|
272
272
|
});
|
|
273
273
|
// Resolve cliId — explicit override wins; otherwise persist on disk
|
|
274
274
|
// so subsequent refreshes present the same device identity.
|
|
275
|
-
|
|
276
|
-
if (!cliId) {
|
|
277
|
-
cliId = cfg.cliId ?? (0, device_id_1.generateCliId)();
|
|
278
|
-
if (!cfg.cliId) {
|
|
279
|
-
(0, config_1.saveConfig)({ ...cfg, cliId });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
275
|
+
const cliId = opts?.cliId ?? (0, cli_identity_1.cliIdForLogin)(cfg);
|
|
282
276
|
const { codeVerifier, codeChallenge } = generatePkce();
|
|
283
277
|
const state = generateState();
|
|
284
278
|
return new Promise((resolve) => {
|
package/dist/lib/refresh.js
CHANGED
|
@@ -27,6 +27,7 @@ Object.defineProperty(exports, "PermanentAuthFailure", { enumerable: true, get:
|
|
|
27
27
|
Object.defineProperty(exports, "TransientAuthFailure", { enumerable: true, get: function () { return auth_core_1.TransientAuthFailure; } });
|
|
28
28
|
const auth_storage_1 = require("./auth-storage");
|
|
29
29
|
const config_1 = require("./config");
|
|
30
|
+
const cli_id_1 = require("./cli-id");
|
|
30
31
|
const credential_resolver_1 = require("./credential-resolver");
|
|
31
32
|
const device_identity_1 = require("./device-identity");
|
|
32
33
|
exports.CLI_REFRESH_EARLY_MS = 5 * 60 * 1000;
|
|
@@ -182,7 +183,10 @@ async function ensureFreshCliCredential(opts = {}) {
|
|
|
182
183
|
*/
|
|
183
184
|
async function refreshCliToken() {
|
|
184
185
|
const cfg = (0, config_1.loadConfig)();
|
|
185
|
-
|
|
186
|
+
// The id the stored credential was minted under — the machine id, or this
|
|
187
|
+
// profile's own (OSK-12156). One resolver, so refresh cannot disagree with login.
|
|
188
|
+
const cliId = (0, cli_id_1.resolveCliId)(cfg);
|
|
189
|
+
if (!cliId) {
|
|
186
190
|
throw new CliRefreshUnavailableError('No CLI identity persisted (cliId). Run `skrr login` to initialize.');
|
|
187
191
|
}
|
|
188
192
|
const origin = getCliOrigin();
|
|
@@ -195,7 +199,7 @@ async function refreshCliToken() {
|
|
|
195
199
|
if (!bundle?.refreshToken) {
|
|
196
200
|
throw new CliRefreshUnavailableError('No refresh token available — sign in with `skrr login`.');
|
|
197
201
|
}
|
|
198
|
-
const pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, bundle.refreshToken,
|
|
202
|
+
const pair = await (0, auth_core_1.refreshDaemonToken)(serverUrl, bundle.refreshToken, cliId,
|
|
199
203
|
// L11 Phase 3 — attach a device-identity proof to every refresh.
|
|
200
204
|
// The signer returns null when device identity isn't initialized
|
|
201
205
|
// (Phase 1 binary, KEK rotation hot path), and the server's lenient
|
package/dist/lib/tasks.d.ts
CHANGED
|
@@ -8,6 +8,25 @@ import type { ParsedDeliverableCoordinate, ParsedExpectationSpec } from '@skrr-a
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function resolveTaskGoalFlag(value: string | undefined): string | null | undefined;
|
|
10
10
|
export declare const TASK_GOAL_NOT_FOUND = "TASK_GOAL_NOT_FOUND";
|
|
11
|
+
/**
|
|
12
|
+
* `--working-directory` on `tasks create` / `update` / `upsert`. `none`,
|
|
13
|
+
* `null` and `clear` mean "no per-task override": on update the PATCH sends
|
|
14
|
+
* `workingDirectory: null` ($unset → inherit the agent default), on create it
|
|
15
|
+
* stores as absent. The sentinel set mirrors `resolveTaskGoalFlag` — it is the
|
|
16
|
+
* spelling a user who knows `--goal none` reaches for.
|
|
17
|
+
* Undefined means the flag was not passed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function workingDirectoryFromFlag(value: string | undefined): string | null | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Validate a RESOLVED `workingDirectory` payload value (post-merge, so flag
|
|
22
|
+
* and `--from-json` inputs are checked identically). `null`/`undefined` are
|
|
23
|
+
* always fine — they mean "no override". A string must be absolute: the path
|
|
24
|
+
* is resolved on the daemon's filesystem, so a relative value can neither be
|
|
25
|
+
* verified here nor trusted there — it would fall back, silently, at run
|
|
26
|
+
* time. Failing here is the same contract as every other bad spec in this
|
|
27
|
+
* file: refuse before the write, never store a value that cannot take effect.
|
|
28
|
+
*/
|
|
29
|
+
export declare function workingDirectoryValidationError(value: unknown): string | null;
|
|
11
30
|
/**
|
|
12
31
|
* The refusal for a goal id that names no goal, in CLI terms.
|
|
13
32
|
*
|
|
@@ -91,6 +110,7 @@ export type TaskCreateFlagValues = {
|
|
|
91
110
|
estimate?: number;
|
|
92
111
|
tag?: string[];
|
|
93
112
|
'label-id'?: string[];
|
|
113
|
+
'working-directory'?: string;
|
|
94
114
|
worktree?: boolean;
|
|
95
115
|
'lifecycle-definition'?: string;
|
|
96
116
|
'lifecycle-version'?: string;
|
package/dist/lib/tasks.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RECOGNIZED_TASK_VERDICTS = exports.DELIVERABLE_LIFECYCLE_VALUES = exports.EXPECT_FLAG_HELP = exports.TASK_WORK_EVENT_KINDS = exports.TASK_PRIORITIES = exports.TASK_GOAL_NOT_FOUND = void 0;
|
|
4
4
|
exports.resolveTaskGoalFlag = resolveTaskGoalFlag;
|
|
5
|
+
exports.workingDirectoryFromFlag = workingDirectoryFromFlag;
|
|
6
|
+
exports.workingDirectoryValidationError = workingDirectoryValidationError;
|
|
5
7
|
exports.describeTaskGoalNotFound = describeTaskGoalNotFound;
|
|
6
8
|
exports.releasePinRepairGuard = releasePinRepairGuard;
|
|
7
9
|
exports.taskUpdateMutationGuard = taskUpdateMutationGuard;
|
|
@@ -38,6 +40,7 @@ exports.verdictIsRecognized = verdictIsRecognized;
|
|
|
38
40
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
39
41
|
const prompt_1 = require("./prompt");
|
|
40
42
|
const projects_1 = require("./projects");
|
|
43
|
+
const node_path_1 = require("node:path");
|
|
41
44
|
/**
|
|
42
45
|
* `--goal` on `tasks update`. `none`, `null` and `clear` detach the goal: PATCH
|
|
43
46
|
* sends `goalId: null`. Anything else names a goal. `clear` is accepted because
|
|
@@ -54,6 +57,47 @@ function resolveTaskGoalFlag(value) {
|
|
|
54
57
|
return v;
|
|
55
58
|
}
|
|
56
59
|
exports.TASK_GOAL_NOT_FOUND = 'TASK_GOAL_NOT_FOUND';
|
|
60
|
+
/**
|
|
61
|
+
* `--working-directory` on `tasks create` / `update` / `upsert`. `none`,
|
|
62
|
+
* `null` and `clear` mean "no per-task override": on update the PATCH sends
|
|
63
|
+
* `workingDirectory: null` ($unset → inherit the agent default), on create it
|
|
64
|
+
* stores as absent. The sentinel set mirrors `resolveTaskGoalFlag` — it is the
|
|
65
|
+
* spelling a user who knows `--goal none` reaches for.
|
|
66
|
+
* Undefined means the flag was not passed.
|
|
67
|
+
*/
|
|
68
|
+
function workingDirectoryFromFlag(value) {
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const v = value.trim();
|
|
72
|
+
if (!v || ['none', 'null', 'clear'].includes(v.toLowerCase()))
|
|
73
|
+
return null;
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate a RESOLVED `workingDirectory` payload value (post-merge, so flag
|
|
78
|
+
* and `--from-json` inputs are checked identically). `null`/`undefined` are
|
|
79
|
+
* always fine — they mean "no override". A string must be absolute: the path
|
|
80
|
+
* is resolved on the daemon's filesystem, so a relative value can neither be
|
|
81
|
+
* verified here nor trusted there — it would fall back, silently, at run
|
|
82
|
+
* time. Failing here is the same contract as every other bad spec in this
|
|
83
|
+
* file: refuse before the write, never store a value that cannot take effect.
|
|
84
|
+
*/
|
|
85
|
+
function workingDirectoryValidationError(value) {
|
|
86
|
+
if (value === undefined || value === null)
|
|
87
|
+
return null;
|
|
88
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
89
|
+
return 'workingDirectory must be an absolute path string (or null for no override)';
|
|
90
|
+
}
|
|
91
|
+
if (value.length > 1024) {
|
|
92
|
+
return 'workingDirectory must be at most 1024 characters';
|
|
93
|
+
}
|
|
94
|
+
if (!(0, node_path_1.isAbsolute)(value.trim())) {
|
|
95
|
+
return (`workingDirectory must be an absolute path on the daemon's filesystem ` +
|
|
96
|
+
`(got '${value}'). ` +
|
|
97
|
+
'A relative path cannot be resolved there — pass the full path, or "none" to clear.');
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
57
101
|
/**
|
|
58
102
|
* The refusal for a goal id that names no goal, in CLI terms.
|
|
59
103
|
*
|
|
@@ -331,6 +375,8 @@ function buildTaskCreatePayload(body, flags = {}) {
|
|
|
331
375
|
// is a Task→Task edge rather than a pointer at a chat message.
|
|
332
376
|
spawnedByTaskId: flags['spawned-by'],
|
|
333
377
|
spawnReason: flags['spawn-reason'],
|
|
378
|
+
// Per-task working-directory override; 'none' ⇒ null (no override).
|
|
379
|
+
workingDirectory: workingDirectoryFromFlag(flags['working-directory']),
|
|
334
380
|
// Worktree isolation opt-in. --worktree ⇒ 'ephemeral', --no-worktree ⇒
|
|
335
381
|
// 'shared', omitted ⇒ undefined (inherit space policy / global default).
|
|
336
382
|
executionIsolation: executionIsolationFromWorktreeFlag(flags.worktree),
|
|
@@ -409,6 +455,9 @@ function validateTaskCreatePayload(payload) {
|
|
|
409
455
|
if (payload.templateId !== undefined && !isBoundedString(payload.templateId, 1, 100)) {
|
|
410
456
|
errors.push('templateId must be a non-empty string at most 100 characters');
|
|
411
457
|
}
|
|
458
|
+
const workingDirectoryError = workingDirectoryValidationError(payload.workingDirectory);
|
|
459
|
+
if (workingDirectoryError)
|
|
460
|
+
errors.push(workingDirectoryError);
|
|
412
461
|
if (payload.lifecycle !== undefined) {
|
|
413
462
|
const binding = payload.lifecycle;
|
|
414
463
|
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
|
|
@@ -538,14 +587,24 @@ async function resolveTaskCreateScope(payload, opts = {}) {
|
|
|
538
587
|
payload.spaceId = parentSpaceId;
|
|
539
588
|
}
|
|
540
589
|
const projectId = stringValue(payload.projectId);
|
|
541
|
-
|
|
590
|
+
// A Mongo `_id` is resolved even beside --space: the server addresses a
|
|
591
|
+
// project by its `id` or its ref only, so passing the `_id` through would
|
|
592
|
+
// fail there with less to say about it (OSK-12054).
|
|
593
|
+
if (projectId && (!stringValue(payload.spaceId) || looksLikeStorageId(projectId))) {
|
|
542
594
|
const project = await getCachedProject(projectId, opts.projectCache);
|
|
543
595
|
resolution.project = project;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
596
|
+
// Accepting a second spelling obliges returning the canonical one: the
|
|
597
|
+
// write carries `id`, never whatever the caller happened to paste.
|
|
598
|
+
const canonicalId = stringValue(project.id);
|
|
599
|
+
if (canonicalId && canonicalId !== projectId)
|
|
600
|
+
payload.projectId = canonicalId;
|
|
601
|
+
if (!stringValue(payload.spaceId)) {
|
|
602
|
+
const projectSpaceId = stringValue(project.spaceId);
|
|
603
|
+
if (!projectSpaceId) {
|
|
604
|
+
throw new Error(`Project ${projectId} did not return an owning space`);
|
|
605
|
+
}
|
|
606
|
+
payload.spaceId = projectSpaceId;
|
|
547
607
|
}
|
|
548
|
-
payload.spaceId = projectSpaceId;
|
|
549
608
|
}
|
|
550
609
|
return resolution;
|
|
551
610
|
}
|
|
@@ -756,6 +815,11 @@ function createPatchFromCreatePayload(payload) {
|
|
|
756
815
|
'executionMode',
|
|
757
816
|
'tags',
|
|
758
817
|
'labelIds',
|
|
818
|
+
// Per-task working-directory override — part of the same execution shape
|
|
819
|
+
// the create payload carries, and equally meaningful on the update leg of
|
|
820
|
+
// an upsert. Absent here, `upsert --working-directory` silently no-ops on
|
|
821
|
+
// an existing task.
|
|
822
|
+
'workingDirectory',
|
|
759
823
|
];
|
|
760
824
|
for (const field of copyFields) {
|
|
761
825
|
if (payload[field] !== undefined)
|
|
@@ -840,21 +904,36 @@ async function getCachedTask(taskIdValue, cache) {
|
|
|
840
904
|
}
|
|
841
905
|
return cache.get(taskIdValue);
|
|
842
906
|
}
|
|
907
|
+
/** A 24-hex Mongo ObjectId — the storage `_id` entity JSON prints beside `id`. */
|
|
908
|
+
const STORAGE_ID_RE = /^[0-9a-f]{24}$/i;
|
|
909
|
+
function looksLikeStorageId(value) {
|
|
910
|
+
return STORAGE_ID_RE.test(value);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Entity JSON prints both `id` (canonical) and `_id` (storage) with nothing
|
|
914
|
+
* saying which one commands take, so a pasted `_id` is accepted as the same
|
|
915
|
+
* project rather than refused as unknown (OSK-12054). The caller then writes
|
|
916
|
+
* the canonical `id` — see `resolveTaskCreateScope`.
|
|
917
|
+
*/
|
|
918
|
+
function projectMatchesId(project, ref) {
|
|
919
|
+
return String(project.id ?? '') === ref || String(project._id ?? '') === ref;
|
|
920
|
+
}
|
|
843
921
|
async function getCachedProject(projectId, cache) {
|
|
844
922
|
const load = async () => {
|
|
845
923
|
const filtered = await projects_1.projectApi.search({ q: projectId, limit: 20 });
|
|
846
|
-
let exact = (0, projects_1.projectsFrom)(filtered).filter((project) =>
|
|
924
|
+
let exact = (0, projects_1.projectsFrom)(filtered).filter((project) => projectMatchesId(project, projectId));
|
|
847
925
|
// The cross-space endpoint's `q` contract covers title/description/ref,
|
|
848
926
|
// not UUID. Fall back to the bounded visible project catalog so canonical
|
|
849
927
|
// IDs still resolve without inventing a second project read endpoint.
|
|
850
928
|
if (exact.length === 0) {
|
|
851
929
|
const catalog = await projects_1.projectApi.search({ limit: 200 });
|
|
852
|
-
exact = (0, projects_1.projectsFrom)(catalog).filter((project) =>
|
|
930
|
+
exact = (0, projects_1.projectsFrom)(catalog).filter((project) => projectMatchesId(project, projectId));
|
|
853
931
|
}
|
|
854
932
|
if (exact.length !== 1) {
|
|
855
933
|
throw new Error(exact.length > 1
|
|
856
934
|
? `Project id ${projectId} matched multiple projects`
|
|
857
|
-
: `Project
|
|
935
|
+
: `Project ${projectId} was not found among the projects you can see. Pass the ` +
|
|
936
|
+
"project's `id` (the uuid `projects list` prints), or its ref (OSK-P-5) together with --space");
|
|
858
937
|
}
|
|
859
938
|
return exact[0];
|
|
860
939
|
};
|
|
@@ -21,6 +21,8 @@ type DisabledReason = 'kek_unavailable' | 'init_failed' | 'platform_unsupported'
|
|
|
21
21
|
export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelopeState;
|
|
22
22
|
/** @internal Test seam — read state for assertions. */
|
|
23
23
|
export declare function __getStateForTest(): CredEnvelopeState;
|
|
24
|
+
/** @internal Test seam — each spec owns its warn-dedupe horizon. */
|
|
25
|
+
export declare function __clearWarnDedupeForTest(): void;
|
|
24
26
|
/**
|
|
25
27
|
* Initialize the envelope path. Idempotent — second call when state is
|
|
26
28
|
* non-uninit returns immediately.
|