@skrr-ai/cli 0.1.49 → 0.1.51
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 +5 -4
- package/dist/base-command.js +75 -6
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +9 -0
- package/dist/commands/agents/update.js +38 -5
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +49 -5
- package/dist/commands/code/run.js +5 -1
- package/dist/commands/commitments/cycles.d.ts +19 -0
- package/dist/commands/commitments/cycles.js +46 -0
- package/dist/commands/commitments/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +40 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/commitments/next-tick.js +3 -1
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +38 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- 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/logout.js +12 -1
- package/dist/commands/machines/dedicated/index.js +1 -1
- package/dist/commands/machines/dedicated/sign-in.js +4 -1
- package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
- package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
- package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
- package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
- package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
- package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
- package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
- package/dist/commands/machines/dedicated/terminal.js +27 -2
- package/dist/commands/machines/hosted/connect.js +2 -2
- package/dist/commands/machines/hosted/destroy.js +1 -1
- package/dist/commands/machines/hosted/exec.js +1 -1
- package/dist/commands/machines/hosted/list.js +1 -1
- package/dist/commands/machines/hosted/pause.js +1 -1
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/resume.js +1 -1
- package/dist/commands/machines/hosted/start.js +2 -2
- package/dist/commands/machines/hosted/status.js +2 -1
- 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 +1 -0
- package/dist/commands/tasks/create.js +6 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +22 -0
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/agentic-stream.d.ts +68 -1
- package/dist/lib/agentic-stream.js +294 -7
- package/dist/lib/api-fetch.js +19 -0
- 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 +65 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +37 -2
- package/dist/lib/commitments.js +148 -0
- package/dist/lib/daemonBroker.d.ts +134 -31
- package/dist/lib/daemonBroker.js +376 -31
- package/dist/lib/dedicated-lease-command.d.ts +14 -0
- package/dist/lib/dedicated-lease-command.js +30 -1
- package/dist/lib/dedicated-machines.js +8 -25
- package/dist/lib/dedicated-service-command.d.ts +11 -3
- package/dist/lib/dedicated-service-command.js +20 -3
- package/dist/lib/dedicated-service.d.ts +46 -0
- package/dist/lib/dedicated-service.js +85 -7
- package/dist/lib/dedicated-terminal.d.ts +21 -0
- package/dist/lib/dedicated-terminal.js +104 -9
- package/dist/lib/delegated-cli.js +4 -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.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +13 -1
- package/dist/lib/first-party-harness-doctor.js +41 -1
- package/dist/lib/first-party-harness-managed.d.ts +4 -3
- package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
- package/dist/lib/first-party-harness-project-trust.js +364 -0
- package/dist/lib/first-party-harness.d.ts +9 -2
- package/dist/lib/first-party-harness.js +6 -5
- package/dist/lib/harnesses.d.ts +10 -0
- package/dist/lib/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- 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 +60 -10
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/node-adapter.js +15 -3
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +54 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
- 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/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
- 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/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
- 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 +16 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
- 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/cjs/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
- 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/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
- 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/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
- 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 +16 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
- 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/dist/esm/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
- package/oclif.manifest.json +24813 -24242
- package/package.json +4 -1
|
@@ -50,6 +50,7 @@ exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
|
|
|
50
50
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
51
51
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
52
52
|
const api_fetch_1 = require("./api-fetch");
|
|
53
|
+
const machine_spend_cap_1 = require("./machine-spend-cap");
|
|
53
54
|
const edge_error_page_1 = require("./edge-error-page");
|
|
54
55
|
const harnesses_1 = require("./harnesses");
|
|
55
56
|
exports.DEDICATED_LEASE_ACTIONS = [
|
|
@@ -1185,7 +1186,7 @@ function dedicatedRuntimeApiErrorDetails(err) {
|
|
|
1185
1186
|
*/
|
|
1186
1187
|
function describeRefusalReasons(err, context = {}) {
|
|
1187
1188
|
const details = dedicatedRuntimeApiErrorDetails(err);
|
|
1188
|
-
const spend = describeSpendingCapRefusal(err.body,
|
|
1189
|
+
const spend = describeSpendingCapRefusal(err.body, context);
|
|
1189
1190
|
if (spend)
|
|
1190
1191
|
return spend;
|
|
1191
1192
|
const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
|
|
@@ -1197,31 +1198,13 @@ function describeRefusalReasons(err, context = {}) {
|
|
|
1197
1198
|
* the cap. Without them "would exceed the monthly cap" left a user guessing by
|
|
1198
1199
|
* how much, and which cap (OSK-9552).
|
|
1199
1200
|
*/
|
|
1200
|
-
function describeSpendingCapRefusal(body,
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
catch {
|
|
1206
|
-
return '';
|
|
1207
|
-
}
|
|
1208
|
-
if (code !== 'RUNTIME_SPENDING_LIMIT_EXCEEDED' || !details)
|
|
1209
|
-
return '';
|
|
1210
|
-
const cents = (key) => {
|
|
1211
|
-
const value = details[key];
|
|
1212
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
1213
|
-
};
|
|
1214
|
-
const limit = cents('limitCents');
|
|
1215
|
-
const spent = cents('currentSpendCents');
|
|
1216
|
-
const projected = cents('projectedCents');
|
|
1217
|
-
const parts = [
|
|
1218
|
-
limit !== null ? `Cap ${formatUsdCents(limit)}` : '',
|
|
1219
|
-
spent !== null ? `spent this month ${formatUsdCents(spent)}` : '',
|
|
1220
|
-
projected !== null ? `this would bring it to ${formatUsdCents(projected)}` : '',
|
|
1221
|
-
].filter(Boolean);
|
|
1222
|
-
if (parts.length === 0)
|
|
1201
|
+
function describeSpendingCapRefusal(body, context = {}) {
|
|
1202
|
+
// Shared with the hosted CLI (machine-spend-cap.ts), which printed the bare
|
|
1203
|
+
// sentence until OSK-11946.
|
|
1204
|
+
const figures = (0, machine_spend_cap_1.describeMonthlyCapFigures)(body);
|
|
1205
|
+
if (!figures)
|
|
1223
1206
|
return '';
|
|
1224
|
-
return ` ${
|
|
1207
|
+
return ` ${figures}. ${raiseCapHint(context)}`;
|
|
1225
1208
|
}
|
|
1226
1209
|
/**
|
|
1227
1210
|
* How to get past a cap refusal, for the command that was refused. A create or
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DedicatedLeaseCommand } from './dedicated-lease-command';
|
|
2
|
-
import { DedicatedServiceChannel, type ServiceVerb } from './dedicated-service';
|
|
2
|
+
import { DedicatedServiceChannel, type ServiceVerb, type TerminalVerb } from './dedicated-service';
|
|
3
3
|
/**
|
|
4
4
|
* The flag set `service create`/`apply` and guest `service propose` share —
|
|
5
5
|
* one definition so the three surfaces cannot drift apart on what a spec
|
|
@@ -40,6 +40,12 @@ export declare abstract class DedicatedServiceCommand extends DedicatedLeaseComm
|
|
|
40
40
|
protected serviceChannel(): Promise<DedicatedServiceChannel>;
|
|
41
41
|
/** Emit one verb and return the daemon's `result`, or throw as a CLI error. */
|
|
42
42
|
protected callService<T = unknown>(verb: ServiceVerb, payload: Record<string, unknown>, timeoutMs?: number): Promise<T | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Emit one persistent-terminal verb (`dedicated:terminal:list|kill|rename`)
|
|
45
|
+
* on the same admitted channel the service verbs ride — the relay answers
|
|
46
|
+
* `dedicated:terminal:ack` with the same requestId discipline.
|
|
47
|
+
*/
|
|
48
|
+
protected callDedicatedTerminal<T = unknown>(verb: TerminalVerb, payload: Record<string, unknown>, timeoutMs?: number): Promise<T | undefined>;
|
|
43
49
|
/**
|
|
44
50
|
* `start|stop|restart|rm` — the shared `dedicated:service:action` round
|
|
45
51
|
* trip and its printout. `rm` maps to the `remove` action on the wire (the
|
|
@@ -54,7 +60,9 @@ export declare abstract class DedicatedServiceCommand extends DedicatedLeaseComm
|
|
|
54
60
|
/**
|
|
55
61
|
* Turn a service-lane failure into the command's exit — the refusal
|
|
56
62
|
* sentence the relay sent, or the clean capability-missing path the SSH
|
|
57
|
-
* lane established for a missing `sshBridge`.
|
|
63
|
+
* lane established for a missing `sshBridge`. `unsupportedCode` is the
|
|
64
|
+
* lane's own code a `--json` error reports; the terminal lane passes its
|
|
65
|
+
* own so a script can tell "no persistent terminals" from "no services".
|
|
58
66
|
*/
|
|
59
|
-
protected reportServiceError(err: unknown): never;
|
|
67
|
+
protected reportServiceError(err: unknown, unsupportedCode?: string): never;
|
|
60
68
|
}
|
|
@@ -94,6 +94,21 @@ class DedicatedServiceCommand extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
94
94
|
this.reportServiceError(err);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Emit one persistent-terminal verb (`dedicated:terminal:list|kill|rename`)
|
|
99
|
+
* on the same admitted channel the service verbs ride — the relay answers
|
|
100
|
+
* `dedicated:terminal:ack` with the same requestId discipline.
|
|
101
|
+
*/
|
|
102
|
+
async callDedicatedTerminal(verb, payload, timeoutMs) {
|
|
103
|
+
try {
|
|
104
|
+
const channel = await this.serviceChannel();
|
|
105
|
+
const { result } = await channel.callTerminal(verb, payload, timeoutMs);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
this.reportServiceError(err, 'DEDICATED_RUNTIME_TERMINAL_UNSUPPORTED');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
97
112
|
/**
|
|
98
113
|
* `start|stop|restart|rm` — the shared `dedicated:service:action` round
|
|
99
114
|
* trip and its printout. `rm` maps to the `remove` action on the wire (the
|
|
@@ -143,14 +158,16 @@ class DedicatedServiceCommand extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
143
158
|
/**
|
|
144
159
|
* Turn a service-lane failure into the command's exit — the refusal
|
|
145
160
|
* sentence the relay sent, or the clean capability-missing path the SSH
|
|
146
|
-
* lane established for a missing `sshBridge`.
|
|
161
|
+
* lane established for a missing `sshBridge`. `unsupportedCode` is the
|
|
162
|
+
* lane's own code a `--json` error reports; the terminal lane passes its
|
|
163
|
+
* own so a script can tell "no persistent terminals" from "no services".
|
|
147
164
|
*/
|
|
148
|
-
reportServiceError(err) {
|
|
165
|
+
reportServiceError(err, unsupportedCode = 'DEDICATED_RUNTIME_SERVICES_UNSUPPORTED') {
|
|
149
166
|
if (err instanceof dedicated_service_1.DedicatedServiceError) {
|
|
150
167
|
if ((0, dedicated_service_1.isServiceUnsupportedCode)(err.code)) {
|
|
151
168
|
this.failWithCliError({
|
|
152
169
|
message: err.message,
|
|
153
|
-
code:
|
|
170
|
+
code: unsupportedCode,
|
|
154
171
|
exit: 1,
|
|
155
172
|
retryable: false,
|
|
156
173
|
});
|
|
@@ -43,6 +43,31 @@ export declare const DEDICATED_SERVICE_EVENTS: {
|
|
|
43
43
|
readonly logClosed: "dedicated:service:log:closed";
|
|
44
44
|
readonly proposed: "dedicated:service:proposed";
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Persistent-terminal management (always-on §6.4) — the sibling lane on the
|
|
48
|
+
* same admitted socket. A session is a tmux `skrr-term-<name>` the machine
|
|
49
|
+
* keeps alive across attaches; `terminal` opens one over the ordinary
|
|
50
|
+
* terminal start lane, and these verbs enumerate, end and rename it.
|
|
51
|
+
*/
|
|
52
|
+
export declare const DEDICATED_TERMINAL_EVENTS: {
|
|
53
|
+
readonly list: "dedicated:terminal:list";
|
|
54
|
+
readonly kill: "dedicated:terminal:kill";
|
|
55
|
+
readonly rename: "dedicated:terminal:rename";
|
|
56
|
+
readonly ack: "dedicated:terminal:ack";
|
|
57
|
+
};
|
|
58
|
+
export type TerminalVerb = 'list' | 'kill' | 'rename';
|
|
59
|
+
/** `skrr-term-<name>`'s suffix — the pattern the relay and daemon both enforce. */
|
|
60
|
+
export declare const DEDICATED_TERMINAL_NAME_PATTERN: RegExp;
|
|
61
|
+
/** The wire capability a live daemon must announce for persistent terminals. */
|
|
62
|
+
export declare const DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = "persistent_terminal_v1";
|
|
63
|
+
/** One `dedicated:terminal:list` row — the tmux session under its suffix. */
|
|
64
|
+
export interface DedicatedTerminalSessionInfo {
|
|
65
|
+
name: string;
|
|
66
|
+
/** tmux `session_created` — SECONDS when numeric, not milliseconds. */
|
|
67
|
+
createdAt?: string | number;
|
|
68
|
+
attached?: boolean;
|
|
69
|
+
windows?: number;
|
|
70
|
+
}
|
|
46
71
|
/** How long a verb has to answer before the machine is declared unreachable. */
|
|
47
72
|
export declare const SERVICE_CALL_TIMEOUT_MS = 30000;
|
|
48
73
|
/** §5.3 — the scope name `skrr-svc-<name>` derives from it. */
|
|
@@ -167,6 +192,17 @@ export declare function describeServiceRefusal(ack: Pick<DedicatedServiceAck, 'c
|
|
|
167
192
|
verb?: string;
|
|
168
193
|
name?: string;
|
|
169
194
|
}): string;
|
|
195
|
+
/**
|
|
196
|
+
* A persistent-terminal refusal in the same register `describeServiceRefusal`
|
|
197
|
+
* gives the service lane — a named condition and the next step, never a bare
|
|
198
|
+
* code. `dedicated:terminal:*` acks share this channel's requestId shape, so
|
|
199
|
+
* the same refusal codes arrive; the words name terminal sessions, not specs.
|
|
200
|
+
*/
|
|
201
|
+
export declare function describeTerminalRefusal(ack: Pick<DedicatedServiceAck, 'code' | 'error'>, context?: {
|
|
202
|
+
leaseId?: string;
|
|
203
|
+
verb?: string;
|
|
204
|
+
name?: string;
|
|
205
|
+
}): string;
|
|
170
206
|
/** Validation that costs the daemon nothing to re-check — §5.3's table, enforced client-side first. */
|
|
171
207
|
export declare function validateServiceSpec(doc: unknown, options?: {
|
|
172
208
|
requireName?: boolean;
|
|
@@ -251,6 +287,16 @@ export declare class DedicatedServiceChannel {
|
|
|
251
287
|
result: T | undefined;
|
|
252
288
|
ack: DedicatedServiceAck;
|
|
253
289
|
}>;
|
|
290
|
+
/**
|
|
291
|
+
* A persistent-terminal verb — `dedicated:terminal:list|kill|rename`,
|
|
292
|
+
* answered on `dedicated:terminal:ack`. Same requestId/ack discipline as
|
|
293
|
+
* the service verbs; the event family and the refusal's wording are the
|
|
294
|
+
* only differences.
|
|
295
|
+
*/
|
|
296
|
+
callTerminal<T = unknown>(verb: TerminalVerb, payload: Record<string, unknown>, timeoutMs?: number): Promise<{
|
|
297
|
+
result: T | undefined;
|
|
298
|
+
ack: DedicatedServiceAck;
|
|
299
|
+
}>;
|
|
254
300
|
/** The shared request/ack round-trip. */
|
|
255
301
|
private callEvent;
|
|
256
302
|
/** Subscribe to a server-pushed frame (`state`, `log`, `log:closed`, `proposed`). */
|
|
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.GUEST_SERVICE_SOCKET_PATH = exports.DedicatedServiceChannel = exports.DedicatedServiceError = exports.SERVICE_ACTIONS = exports.SERVICE_RESTART_POLICIES = exports.SERVICE_RESERVED_NAMES = exports.SERVICE_SECRET_NAME_PATTERN = exports.SERVICE_NAME_PATTERN = exports.SERVICE_CALL_TIMEOUT_MS = exports.DEDICATED_SERVICE_EVENTS = void 0;
|
|
6
|
+
exports.GUEST_SERVICE_SOCKET_PATH = exports.DedicatedServiceChannel = exports.DedicatedServiceError = exports.SERVICE_ACTIONS = exports.SERVICE_RESTART_POLICIES = exports.SERVICE_RESERVED_NAMES = exports.SERVICE_SECRET_NAME_PATTERN = exports.SERVICE_NAME_PATTERN = exports.SERVICE_CALL_TIMEOUT_MS = exports.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = exports.DEDICATED_TERMINAL_NAME_PATTERN = exports.DEDICATED_TERMINAL_EVENTS = exports.DEDICATED_SERVICE_EVENTS = void 0;
|
|
7
7
|
exports.isServiceUnsupportedCode = isServiceUnsupportedCode;
|
|
8
8
|
exports.describeServiceRefusal = describeServiceRefusal;
|
|
9
|
+
exports.describeTerminalRefusal = describeTerminalRefusal;
|
|
9
10
|
exports.validateServiceSpec = validateServiceSpec;
|
|
10
11
|
exports.parseServiceSpecDocument = parseServiceSpecDocument;
|
|
11
12
|
exports.specPayload = specPayload;
|
|
@@ -72,6 +73,22 @@ exports.DEDICATED_SERVICE_EVENTS = {
|
|
|
72
73
|
logClosed: 'dedicated:service:log:closed',
|
|
73
74
|
proposed: 'dedicated:service:proposed',
|
|
74
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Persistent-terminal management (always-on §6.4) — the sibling lane on the
|
|
78
|
+
* same admitted socket. A session is a tmux `skrr-term-<name>` the machine
|
|
79
|
+
* keeps alive across attaches; `terminal` opens one over the ordinary
|
|
80
|
+
* terminal start lane, and these verbs enumerate, end and rename it.
|
|
81
|
+
*/
|
|
82
|
+
exports.DEDICATED_TERMINAL_EVENTS = {
|
|
83
|
+
list: 'dedicated:terminal:list',
|
|
84
|
+
kill: 'dedicated:terminal:kill',
|
|
85
|
+
rename: 'dedicated:terminal:rename',
|
|
86
|
+
ack: 'dedicated:terminal:ack',
|
|
87
|
+
};
|
|
88
|
+
/** `skrr-term-<name>`'s suffix — the pattern the relay and daemon both enforce. */
|
|
89
|
+
exports.DEDICATED_TERMINAL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
90
|
+
/** The wire capability a live daemon must announce for persistent terminals. */
|
|
91
|
+
exports.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = 'persistent_terminal_v1';
|
|
75
92
|
/** How long a verb has to answer before the machine is declared unreachable. */
|
|
76
93
|
exports.SERVICE_CALL_TIMEOUT_MS = 30_000;
|
|
77
94
|
/** §5.3 — the scope name `skrr-svc-<name>` derives from it. */
|
|
@@ -106,6 +123,7 @@ exports.DedicatedServiceError = DedicatedServiceError;
|
|
|
106
123
|
function isServiceUnsupportedCode(code) {
|
|
107
124
|
const c = (code || '').toLowerCase();
|
|
108
125
|
return (c === 'dedicated_runtime_services_unsupported' ||
|
|
126
|
+
c === 'dedicated_runtime_terminal_unsupported' ||
|
|
109
127
|
c === 'services_unsupported' ||
|
|
110
128
|
c === 'unsupported' ||
|
|
111
129
|
c === 'not_implemented' ||
|
|
@@ -152,6 +170,48 @@ function describeServiceRefusal(ack, context = {}) {
|
|
|
152
170
|
}
|
|
153
171
|
return ack.error ? `The service request failed: ${ack.error}` : 'The service request failed.';
|
|
154
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* A persistent-terminal refusal in the same register `describeServiceRefusal`
|
|
175
|
+
* gives the service lane — a named condition and the next step, never a bare
|
|
176
|
+
* code. `dedicated:terminal:*` acks share this channel's requestId shape, so
|
|
177
|
+
* the same refusal codes arrive; the words name terminal sessions, not specs.
|
|
178
|
+
*/
|
|
179
|
+
function describeTerminalRefusal(ack, context = {}) {
|
|
180
|
+
const code = ack.code || '';
|
|
181
|
+
if (isServiceUnsupportedCode(code)) {
|
|
182
|
+
return (ack.error ||
|
|
183
|
+
`This machine's image does not support persistent terminals (persistent_terminal_v1). ` +
|
|
184
|
+
`Update it with \`skrr machines dedicated update-image ${context.leaseId || '<lease-id>'} --wait\`; \`skrr machines dedicated terminal\` opens a plain shell in the meantime.`);
|
|
185
|
+
}
|
|
186
|
+
if (code === 'DEDICATED_RUNTIME_NOT_ASSIGNABLE') {
|
|
187
|
+
return ('The machine is not ready to take terminal commands (it must be running with a healthy workspace). ' +
|
|
188
|
+
`Check it with \`skrr machines dedicated show ${context.leaseId || '<lease-id>'}\`.`);
|
|
189
|
+
}
|
|
190
|
+
if (code === 'DEDICATED_RUNTIME_GENERATION_STALE') {
|
|
191
|
+
return 'The machine was replaced since its daemon registered. Wait a minute and try again.';
|
|
192
|
+
}
|
|
193
|
+
if (code === 'dedicated_runtime_quiesced') {
|
|
194
|
+
return 'The machine is checkpointing its workspace right now; try again in a moment.';
|
|
195
|
+
}
|
|
196
|
+
if (code === 'offline' ||
|
|
197
|
+
code === 'daemon_offline' ||
|
|
198
|
+
code === 'daemon-gone' ||
|
|
199
|
+
code === 'daemon-timeout') {
|
|
200
|
+
return 'The machine’s daemon is not answering. Wait for it to come online and try again.';
|
|
201
|
+
}
|
|
202
|
+
if (code === 'not_found') {
|
|
203
|
+
return context.name
|
|
204
|
+
? `No terminal session named ${context.name} on this machine — \`skrr machines dedicated terminal ls ${context.leaseId || '<lease-id>'}\` lists them.`
|
|
205
|
+
: 'The machine or the named session was not found.';
|
|
206
|
+
}
|
|
207
|
+
if (code === 'invalid' || code === 'validation') {
|
|
208
|
+
return `The terminal request was refused: ${ack.error || 'invalid request'}`;
|
|
209
|
+
}
|
|
210
|
+
if (code === 'rate-limited' || code === 'cap') {
|
|
211
|
+
return 'Too many terminal requests in flight — wait a moment and retry.';
|
|
212
|
+
}
|
|
213
|
+
return ack.error ? `The terminal request failed: ${ack.error}` : 'The terminal request failed.';
|
|
214
|
+
}
|
|
155
215
|
/* ---------------------------------------------------------------- spec --- */
|
|
156
216
|
/** Validation that costs the daemon nothing to re-check — §5.3's table, enforced client-side first. */
|
|
157
217
|
function validateServiceSpec(doc, options = {}) {
|
|
@@ -383,18 +443,22 @@ class DedicatedServiceChannel {
|
|
|
383
443
|
this.options = options;
|
|
384
444
|
const connect = options.connect ?? socket_io_client_1.io;
|
|
385
445
|
this.socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), socketOptions(options.token));
|
|
386
|
-
|
|
446
|
+
// An ack resolves a request only when it arrives on THAT request's own
|
|
447
|
+
// ack event — a requestId echoed back on the wrong family is not an
|
|
448
|
+
// answer, however well-formed it looks.
|
|
449
|
+
const onAck = (ackEvent) => (ack) => {
|
|
387
450
|
if (!ack?.requestId)
|
|
388
451
|
return;
|
|
389
452
|
const waiter = this.pending.get(ack.requestId);
|
|
390
|
-
if (!waiter)
|
|
453
|
+
if (!waiter || waiter.ackEvent !== ackEvent)
|
|
391
454
|
return;
|
|
392
455
|
this.pending.delete(ack.requestId);
|
|
393
456
|
clearTimeout(waiter.timer);
|
|
394
457
|
waiter.resolve(ack);
|
|
395
458
|
};
|
|
396
|
-
this.socket.on(exports.DEDICATED_SERVICE_EVENTS.ack, onAck);
|
|
397
|
-
this.socket.on(exports.DEDICATED_SERVICE_EVENTS.templatesAck, onAck);
|
|
459
|
+
this.socket.on(exports.DEDICATED_SERVICE_EVENTS.ack, onAck(exports.DEDICATED_SERVICE_EVENTS.ack));
|
|
460
|
+
this.socket.on(exports.DEDICATED_SERVICE_EVENTS.templatesAck, onAck(exports.DEDICATED_SERVICE_EVENTS.templatesAck));
|
|
461
|
+
this.socket.on(exports.DEDICATED_TERMINAL_EVENTS.ack, onAck(exports.DEDICATED_TERMINAL_EVENTS.ack));
|
|
398
462
|
}
|
|
399
463
|
/** Resolves once the transport is up; rejects on connect failure. */
|
|
400
464
|
open() {
|
|
@@ -455,8 +519,21 @@ class DedicatedServiceChannel {
|
|
|
455
519
|
verb: `templates:${verb}`,
|
|
456
520
|
}, timeoutMs);
|
|
457
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* A persistent-terminal verb — `dedicated:terminal:list|kill|rename`,
|
|
524
|
+
* answered on `dedicated:terminal:ack`. Same requestId/ack discipline as
|
|
525
|
+
* the service verbs; the event family and the refusal's wording are the
|
|
526
|
+
* only differences.
|
|
527
|
+
*/
|
|
528
|
+
callTerminal(verb, payload, timeoutMs = exports.SERVICE_CALL_TIMEOUT_MS) {
|
|
529
|
+
return this.callEvent(`dedicated:terminal:${verb}`, exports.DEDICATED_TERMINAL_EVENTS.ack, payload, {
|
|
530
|
+
leaseId: typeof payload.leaseId === 'string' ? payload.leaseId : undefined,
|
|
531
|
+
verb: `terminal:${verb}`,
|
|
532
|
+
name: typeof payload.name === 'string' ? payload.name : undefined,
|
|
533
|
+
}, timeoutMs, describeTerminalRefusal);
|
|
534
|
+
}
|
|
458
535
|
/** The shared request/ack round-trip. */
|
|
459
|
-
callEvent(event, ackEvent, payload, context, timeoutMs) {
|
|
536
|
+
callEvent(event, ackEvent, payload, context, timeoutMs, describe = describeServiceRefusal) {
|
|
460
537
|
const requestId = `svc-${(0, node_crypto_1.randomUUID)()}`;
|
|
461
538
|
return new Promise((resolve, reject) => {
|
|
462
539
|
if (this.dead) {
|
|
@@ -469,9 +546,10 @@ class DedicatedServiceChannel {
|
|
|
469
546
|
}, timeoutMs);
|
|
470
547
|
this.pending.set(requestId, {
|
|
471
548
|
timer,
|
|
549
|
+
ackEvent,
|
|
472
550
|
resolve: (ack) => {
|
|
473
551
|
if (!ack.ok) {
|
|
474
|
-
reject(new DedicatedServiceError(
|
|
552
|
+
reject(new DedicatedServiceError(describe(ack, context), ack.code || 'SERVICE_REFUSED'));
|
|
475
553
|
return;
|
|
476
554
|
}
|
|
477
555
|
resolve({ result: ack.result, ack });
|
|
@@ -34,6 +34,8 @@ export interface TerminalStartAck {
|
|
|
34
34
|
ok?: boolean;
|
|
35
35
|
error?: string;
|
|
36
36
|
code?: string;
|
|
37
|
+
/** The tmux session a persistent start attached (`skrr-term-<name>`'s suffix). */
|
|
38
|
+
persistentName?: string;
|
|
37
39
|
}
|
|
38
40
|
/** Bytes of one output frame, without splitting a UTF-8 or ANSI sequence. */
|
|
39
41
|
export declare function decodeTerminalFrame(frame: TerminalOutputFrame): Buffer;
|
|
@@ -56,6 +58,25 @@ export interface DedicatedTerminalOptions {
|
|
|
56
58
|
daemonId: string;
|
|
57
59
|
cwd?: string;
|
|
58
60
|
initialCommand?: string;
|
|
61
|
+
/**
|
|
62
|
+
* A tmux-backed session (always-on §6.4): the machine keeps it alive across
|
|
63
|
+
* attaches, disconnects and daemon restarts, and a reconnect re-attaches it
|
|
64
|
+
* by name instead of opening a new shell. A daemon that predates
|
|
65
|
+
* `persistent_terminal_v1` silently ignores it — callers gate on the
|
|
66
|
+
* capability before asking.
|
|
67
|
+
*/
|
|
68
|
+
persistent?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The session suffix (`skrr-term-<name>`). The same name re-attaches the
|
|
71
|
+
* same session; omitted under `persistent`, the machine names the session
|
|
72
|
+
* `term-N` and the start ack reports it as `persistentName`.
|
|
73
|
+
*/
|
|
74
|
+
sessionName?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The lease this terminal claims — the relay refuses a start that names a
|
|
77
|
+
* lease but resolves to another lease's daemon.
|
|
78
|
+
*/
|
|
79
|
+
leaseId?: string;
|
|
59
80
|
input: NodeJS.ReadStream;
|
|
60
81
|
output: NodeJS.WriteStream;
|
|
61
82
|
errorOutput: NodeJS.WritableStream;
|
|
@@ -78,13 +78,22 @@ function describeTerminalStartRefusal(ack) {
|
|
|
78
78
|
*/
|
|
79
79
|
function runDedicatedTerminal(options) {
|
|
80
80
|
const events = data_provider_1.HARNESS_TERMINAL_EVENTS;
|
|
81
|
-
const
|
|
81
|
+
const persistent = options.persistent === true;
|
|
82
82
|
const connect = options.connect ?? socket_io_client_1.io;
|
|
83
83
|
const { input, output, errorOutput } = options;
|
|
84
84
|
return new Promise((resolve, reject) => {
|
|
85
|
+
// The daemon keys a terminal by the sessionId in its start — one attach.
|
|
86
|
+
// A persistent session re-attaches across socket reconnects, and each
|
|
87
|
+
// attach is a fresh id over the same tmux name.
|
|
88
|
+
let sessionId = '';
|
|
85
89
|
let started = false;
|
|
86
90
|
let finished = false;
|
|
87
91
|
let rawMode = false;
|
|
92
|
+
// First attach armed the tty; a re-attach needs only the ack, not the
|
|
93
|
+
// setup again.
|
|
94
|
+
let sessionReady = false;
|
|
95
|
+
let reattaching = false;
|
|
96
|
+
let persistentName = options.sessionName;
|
|
88
97
|
const socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), {
|
|
89
98
|
path: '/ws/agentic',
|
|
90
99
|
auth: {
|
|
@@ -94,10 +103,15 @@ function runDedicatedTerminal(options) {
|
|
|
94
103
|
clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
95
104
|
},
|
|
96
105
|
transports: ['websocket'],
|
|
97
|
-
// A
|
|
106
|
+
// A bare PTY cannot be resumed: the relay forgets the session with the
|
|
98
107
|
// socket, and silently opening a NEW shell would drop the user into a
|
|
99
|
-
// different process tree mid-command
|
|
100
|
-
|
|
108
|
+
// different process tree mid-command — say it ended instead. A
|
|
109
|
+
// persistent session is the opposite by design: tmux keeps it, and the
|
|
110
|
+
// reconnect re-attaches it BY NAME, which is the same shell.
|
|
111
|
+
reconnection: persistent,
|
|
112
|
+
reconnectionAttempts: 10,
|
|
113
|
+
reconnectionDelay: 1_000,
|
|
114
|
+
reconnectionDelayMax: 5_000,
|
|
101
115
|
});
|
|
102
116
|
// Typed only once the shell's line editor says it is reading a command line
|
|
103
117
|
// (`createPendingShellCommand`, the rule the web terminal reads too). A
|
|
@@ -106,7 +120,10 @@ function runDedicatedTerminal(options) {
|
|
|
106
120
|
const pending = initialCommand
|
|
107
121
|
? (0, data_provider_1.createPendingShellCommand)({
|
|
108
122
|
command: initialCommand,
|
|
109
|
-
send: (data) =>
|
|
123
|
+
send: (data) => {
|
|
124
|
+
if (started)
|
|
125
|
+
socket.emit(events.input, { sessionId, data });
|
|
126
|
+
},
|
|
110
127
|
onStatusChange: (status) => {
|
|
111
128
|
const notice = describePendingCommand(status, initialCommand);
|
|
112
129
|
if (notice && !finished)
|
|
@@ -129,13 +146,20 @@ function runDedicatedTerminal(options) {
|
|
|
129
146
|
if (detachAt > 0) {
|
|
130
147
|
const typed = buffer.subarray(0, detachAt);
|
|
131
148
|
pending?.observeUserInput(typed);
|
|
132
|
-
|
|
149
|
+
if (started)
|
|
150
|
+
socket.emit(events.input, { sessionId, data: typed.toString('utf8') });
|
|
133
151
|
}
|
|
134
|
-
errorOutput.write(
|
|
152
|
+
errorOutput.write(persistent && persistentName
|
|
153
|
+
? `\r\n[detached — session '${persistentName}' keeps running on the machine]\r\n`
|
|
154
|
+
: '\r\n[closed the terminal]\r\n');
|
|
135
155
|
finish(0, true);
|
|
136
156
|
return;
|
|
137
157
|
}
|
|
138
158
|
pending?.observeUserInput(buffer);
|
|
159
|
+
// While a reconnect is in flight the attach is gone: the frame would be
|
|
160
|
+
// dropped daemon-side anyway, so it is not sent.
|
|
161
|
+
if (!started)
|
|
162
|
+
return;
|
|
139
163
|
socket.emit(events.input, { sessionId, data: buffer.toString('utf8') });
|
|
140
164
|
};
|
|
141
165
|
// A terminal that closes (SIGHUP) or a supervisor that stops us (SIGTERM):
|
|
@@ -145,9 +169,11 @@ function runDedicatedTerminal(options) {
|
|
|
145
169
|
// SIGINT's status, for both.
|
|
146
170
|
const onSigterm = () => finish(143, true);
|
|
147
171
|
const onSighup = () => finish(129, true);
|
|
148
|
-
const
|
|
172
|
+
const onStartTimeout = () => {
|
|
149
173
|
fail(new DedicatedTerminalError('The machine did not answer the terminal start in time. Its daemon may be busy or offline.', 'TERMINAL_START_TIMEOUT'));
|
|
150
|
-
}
|
|
174
|
+
};
|
|
175
|
+
let startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
|
|
176
|
+
startTimer.unref();
|
|
151
177
|
function restoreTty() {
|
|
152
178
|
input.off('data', onInput);
|
|
153
179
|
output.off('resize', onResize);
|
|
@@ -166,6 +192,7 @@ function runDedicatedTerminal(options) {
|
|
|
166
192
|
clearTimeout(startTimer);
|
|
167
193
|
pending?.close();
|
|
168
194
|
restoreTty();
|
|
195
|
+
detachReconnectFailed();
|
|
169
196
|
if (stopRemote && started)
|
|
170
197
|
socket.emit(events.stop, { sessionId });
|
|
171
198
|
socket.disconnect();
|
|
@@ -178,17 +205,34 @@ function runDedicatedTerminal(options) {
|
|
|
178
205
|
clearTimeout(startTimer);
|
|
179
206
|
pending?.close();
|
|
180
207
|
restoreTty();
|
|
208
|
+
detachReconnectFailed();
|
|
181
209
|
socket.disconnect();
|
|
182
210
|
reject(error);
|
|
183
211
|
}
|
|
184
212
|
socket.on('connect_error', (err) => {
|
|
213
|
+
// A failed retry while reattaching is not fatal — the attempt countdown
|
|
214
|
+
// keeps running and `reconnect_failed` ends it if they all miss.
|
|
215
|
+
if (persistent && reattaching)
|
|
216
|
+
return;
|
|
185
217
|
fail(new DedicatedTerminalError(`Could not connect to skrr: ${err.message}`, err.data?.code || 'TERMINAL_CONNECT_FAILED'));
|
|
186
218
|
});
|
|
187
219
|
socket.on('connect', () => {
|
|
220
|
+
if (finished)
|
|
221
|
+
return;
|
|
222
|
+
sessionId = (0, node_crypto_1.randomUUID)();
|
|
223
|
+
clearTimeout(startTimer);
|
|
224
|
+
startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
|
|
225
|
+
startTimer.unref();
|
|
188
226
|
socket.emit(events.start, {
|
|
189
227
|
runtimeId: options.runtimeId,
|
|
190
228
|
sessionId,
|
|
191
229
|
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
230
|
+
...(persistent ? { persistent: true } : {}),
|
|
231
|
+
// The RESOLVED name, not just the requested one: a `--new` session the
|
|
232
|
+
// machine named itself comes back in the ack, and a re-attach must
|
|
233
|
+
// name it — `tmux -A` without `-s` would open a fresh session instead.
|
|
234
|
+
...(persistentName ? { sessionName: persistentName } : {}),
|
|
235
|
+
...(options.leaseId ? { leaseId: options.leaseId } : {}),
|
|
192
236
|
...size(),
|
|
193
237
|
});
|
|
194
238
|
});
|
|
@@ -201,8 +245,20 @@ function runDedicatedTerminal(options) {
|
|
|
201
245
|
fail(new DedicatedTerminalError(describeTerminalStartRefusal(ack), ack.code || 'TERMINAL_START_REFUSED'));
|
|
202
246
|
return;
|
|
203
247
|
}
|
|
248
|
+
const reattached = reattaching;
|
|
204
249
|
started = true;
|
|
250
|
+
reattaching = false;
|
|
205
251
|
clearTimeout(startTimer);
|
|
252
|
+
if (typeof ack.persistentName === 'string' && ack.persistentName) {
|
|
253
|
+
persistentName = ack.persistentName;
|
|
254
|
+
}
|
|
255
|
+
if (sessionReady) {
|
|
256
|
+
if (reattached) {
|
|
257
|
+
errorOutput.write(`\r\n[reattached to session '${persistentName}']\r\n`);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
sessionReady = true;
|
|
206
262
|
if (typeof input.setRawMode === 'function') {
|
|
207
263
|
input.setRawMode(true);
|
|
208
264
|
rawMode = true;
|
|
@@ -213,6 +269,9 @@ function runDedicatedTerminal(options) {
|
|
|
213
269
|
process.on('SIGTERM', onSigterm);
|
|
214
270
|
process.on('SIGHUP', onSighup);
|
|
215
271
|
pending?.beginSession();
|
|
272
|
+
if (persistent && persistentName) {
|
|
273
|
+
errorOutput.write(`\r\n[session '${persistentName}' is kept alive on the machine — Ctrl-] detaches, the same command re-attaches]\r\n`);
|
|
274
|
+
}
|
|
216
275
|
});
|
|
217
276
|
socket.on(events.output, (frame) => {
|
|
218
277
|
if (!frame || frame.sessionId !== sessionId || finished)
|
|
@@ -233,9 +292,45 @@ function runDedicatedTerminal(options) {
|
|
|
233
292
|
output.write(bytes);
|
|
234
293
|
pending?.observeOutput(bytes);
|
|
235
294
|
});
|
|
295
|
+
// socket.io reports a given-up reconnect on the manager; a bare EventEmitter
|
|
296
|
+
// test socket has none, so the socket itself carries the listener then.
|
|
297
|
+
const onReconnectFailed = () => {
|
|
298
|
+
if (finished || !persistent)
|
|
299
|
+
return;
|
|
300
|
+
const named = persistentName ? ` '${persistentName}'` : '';
|
|
301
|
+
errorOutput.write(`\r\n[could not reconnect to skrr — session${named} still runs on the machine; rerun the same command to reattach]\r\n`);
|
|
302
|
+
finish(1, false);
|
|
303
|
+
};
|
|
304
|
+
const manager = socket.io;
|
|
305
|
+
const onManager = typeof manager?.on === 'function';
|
|
306
|
+
if (onManager)
|
|
307
|
+
manager.on('reconnect_failed', onReconnectFailed);
|
|
308
|
+
else
|
|
309
|
+
socket.on('reconnect_failed', onReconnectFailed);
|
|
310
|
+
// The manager is SHARED per url+path (socket.io multiplexing), so the
|
|
311
|
+
// listener outlives this terminal unless it is removed on every exit.
|
|
312
|
+
function detachReconnectFailed() {
|
|
313
|
+
if (onManager && typeof manager?.off === 'function') {
|
|
314
|
+
manager.off('reconnect_failed', onReconnectFailed);
|
|
315
|
+
}
|
|
316
|
+
else if (!onManager) {
|
|
317
|
+
socket.off('reconnect_failed', onReconnectFailed);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
236
320
|
socket.on('disconnect', () => {
|
|
237
321
|
if (finished)
|
|
238
322
|
return;
|
|
323
|
+
if (persistent) {
|
|
324
|
+
// The tmux session outlives the link — the tmux client's death only
|
|
325
|
+
// detaches. socket.io retries and the next start re-attaches by name.
|
|
326
|
+
started = false;
|
|
327
|
+
if (!reattaching) {
|
|
328
|
+
reattaching = true;
|
|
329
|
+
const named = persistentName ? ` '${persistentName}'` : '';
|
|
330
|
+
errorOutput.write(`\r\n[lost the connection to skrr — reattaching to session${named}…]\r\n`);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
239
334
|
if (!started) {
|
|
240
335
|
fail(new DedicatedTerminalError('The connection to skrr closed before the terminal started.', 'TERMINAL_DISCONNECTED'));
|
|
241
336
|
return;
|
|
@@ -49,6 +49,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
49
49
|
const os = __importStar(require("node:os"));
|
|
50
50
|
const path = __importStar(require("node:path"));
|
|
51
51
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
52
|
+
const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
|
|
52
53
|
function isDelegatedCliSession(env = process.env) {
|
|
53
54
|
return Boolean(env.OVERSKY_AUTONOMOUS_SESSION === '1' &&
|
|
54
55
|
env.OVERSKY_DELEGATED_CLI_BROKER_URL?.trim() &&
|
|
@@ -213,7 +214,9 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
|
|
|
213
214
|
try {
|
|
214
215
|
let res;
|
|
215
216
|
try {
|
|
216
|
-
|
|
217
|
+
// Proxy-free and loopback-only (OSK-12134): the capability and the
|
|
218
|
+
// credential it returns must never reach HTTP_PROXY.
|
|
219
|
+
res = await (0, loopback_http_1.loopbackFetch)(brokerUrl, {
|
|
217
220
|
method: 'POST',
|
|
218
221
|
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
219
222
|
body: JSON.stringify({
|
|
@@ -71,7 +71,9 @@ export declare function notInstalledMessage(): string;
|
|
|
71
71
|
* preserving "exit code = the daemon's exit code" semantics that scripts
|
|
72
72
|
* may depend on.
|
|
73
73
|
*/
|
|
74
|
-
export declare function execRuntimeBinary(args: string[]
|
|
74
|
+
export declare function execRuntimeBinary(args: string[], opts?: {
|
|
75
|
+
binary?: string;
|
|
76
|
+
}): Promise<number>;
|
|
75
77
|
/** Output from a short-lived runtime probe such as `skrrd status --json`. */
|
|
76
78
|
export type RuntimeBinaryOutput = {
|
|
77
79
|
code: number;
|
|
@@ -212,9 +212,11 @@ function notInstalledMessage() {
|
|
|
212
212
|
* preserving "exit code = the daemon's exit code" semantics that scripts
|
|
213
213
|
* may depend on.
|
|
214
214
|
*/
|
|
215
|
-
function execRuntimeBinary(args) {
|
|
215
|
+
function execRuntimeBinary(args, opts = {}) {
|
|
216
216
|
return new Promise((resolve, reject) => {
|
|
217
|
-
|
|
217
|
+
// `binary` lets `status` render from the LIVE daemon's program rather than
|
|
218
|
+
// the first one on PATH (OSK-12102); every other verb forwards to PATH.
|
|
219
|
+
const bin = opts.binary ?? findRuntimeBinary();
|
|
218
220
|
if (!bin) {
|
|
219
221
|
reject(new Error('runtime-binary-not-installed'));
|
|
220
222
|
return;
|
|
@@ -223,6 +223,16 @@ export interface ManagedEngineHooks {
|
|
|
223
223
|
* never from the env this hook returned.
|
|
224
224
|
*/
|
|
225
225
|
credentialMode: () => 'managed' | 'byok';
|
|
226
|
+
/**
|
|
227
|
+
* `prepare` fell back to BYOK AND no provider credential will reach the
|
|
228
|
+
* engine — the exact case where the fallback line already warned that the
|
|
229
|
+
* first request will fail. The caller uses this AFTER a non-zero engine
|
|
230
|
+
* exit to tie an opaque engine failure (an `Unexpected server error` that
|
|
231
|
+
* says nothing about auth) back to the missing credential, without
|
|
232
|
+
* refusing the launch up front: the engine can carry a stored credential
|
|
233
|
+
* `hasProviderCredential` cannot see (OSK-11949).
|
|
234
|
+
*/
|
|
235
|
+
unprovisionedByok: () => boolean;
|
|
226
236
|
}
|
|
227
237
|
export interface ManagedEngineHooksInput {
|
|
228
238
|
subcommand: string | null;
|