@skrr-ai/cli 0.1.47 → 0.1.49
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 +1 -1
- package/dist/base-command.d.ts +18 -0
- package/dist/base-command.js +61 -1
- package/dist/commands/agent-worker.d.ts +12 -0
- package/dist/commands/agent-worker.js +140 -0
- package/dist/commands/agents/chat.d.ts +21 -0
- package/dist/commands/agents/chat.js +26 -9
- package/dist/commands/balance/usage/breakdown.d.ts +14 -0
- package/dist/commands/balance/usage/breakdown.js +68 -0
- package/dist/commands/balance/usage.js +24 -0
- package/dist/commands/commerce/entitlements/explain.d.ts +22 -0
- package/dist/commands/commerce/entitlements/explain.js +40 -0
- package/dist/commands/commerce/incidents/audit.d.ts +20 -0
- package/dist/commands/commerce/incidents/audit.js +37 -0
- package/dist/commands/commerce/incidents/list.d.ts +22 -0
- package/dist/commands/commerce/incidents/list.js +49 -0
- package/dist/commands/commerce/incidents/resolve.d.ts +23 -0
- package/dist/commands/commerce/incidents/resolve.js +45 -0
- package/dist/commands/commerce/incidents/retry.d.ts +22 -0
- package/dist/commands/commerce/incidents/retry.js +45 -0
- package/dist/commands/commerce/incidents/show.d.ts +13 -0
- package/dist/commands/commerce/incidents/show.js +30 -0
- package/dist/commands/commerce/offers/approve.d.ts +17 -0
- package/dist/commands/commerce/offers/approve.js +33 -0
- package/dist/commands/commerce/offers/create.d.ts +18 -0
- package/dist/commands/commerce/offers/create.js +34 -0
- package/dist/commands/commerce/offers/publish.d.ts +13 -0
- package/dist/commands/commerce/offers/publish.js +28 -0
- package/dist/commands/commerce/offers/retire.d.ts +17 -0
- package/dist/commands/commerce/offers/retire.js +32 -0
- package/dist/commands/commerce/offers/validate.d.ts +18 -0
- package/dist/commands/commerce/offers/validate.js +33 -0
- package/dist/commands/commerce/products/create.d.ts +10 -0
- package/dist/commands/commerce/products/create.js +25 -0
- package/dist/commands/commerce/promotions/redeem.d.ts +13 -0
- package/dist/commands/commerce/promotions/redeem.js +32 -0
- package/dist/commands/commerce/terms/accept.d.ts +18 -0
- package/dist/commands/commerce/terms/accept.js +39 -0
- package/dist/commands/commerce/terms/show.d.ts +10 -0
- package/dist/commands/commerce/terms/show.js +34 -0
- package/dist/commands/commerce/trials/conversion-preview.d.ts +18 -0
- package/dist/commands/commerce/trials/conversion-preview.js +39 -0
- package/dist/commands/commerce/trials/start.d.ts +23 -0
- package/dist/commands/commerce/trials/start.js +58 -0
- package/dist/commands/commitments/accounting.d.ts +39 -0
- package/dist/commands/commitments/accounting.js +116 -0
- package/dist/commands/commitments/action-history.d.ts +1 -0
- package/dist/commands/commitments/action-history.js +8 -1
- package/dist/commands/commitments/case.d.ts +1 -0
- package/dist/commands/commitments/case.js +19 -3
- package/dist/commands/commitments/cases.d.ts +1 -0
- package/dist/commands/commitments/cases.js +11 -1
- package/dist/commands/commitments/checks.d.ts +1 -0
- package/dist/commands/commitments/checks.js +5 -1
- package/dist/commands/commitments/cost.d.ts +62 -0
- package/dist/commands/commitments/cost.js +130 -0
- package/dist/commands/commitments/create.js +1 -1
- package/dist/commands/commitments/delete.d.ts +1 -0
- package/dist/commands/commitments/delete.js +56 -6
- package/dist/commands/commitments/feedback.js +9 -1
- package/dist/commands/commitments/handover.js +16 -0
- package/dist/commands/commitments/hypotheses.d.ts +1 -0
- package/dist/commands/commitments/hypotheses.js +17 -2
- package/dist/commands/commitments/links/list.js +4 -1
- package/dist/commands/commitments/liveness.d.ts +18 -0
- package/dist/commands/commitments/liveness.js +80 -0
- package/dist/commands/commitments/metrics.d.ts +1 -0
- package/dist/commands/commitments/metrics.js +8 -1
- package/dist/commands/commitments/next-tick.d.ts +39 -0
- package/dist/commands/commitments/next-tick.js +122 -0
- package/dist/commands/commitments/observations.d.ts +1 -0
- package/dist/commands/commitments/observations.js +19 -2
- package/dist/commands/commitments/pause.js +12 -0
- package/dist/commands/commitments/receipt.d.ts +2 -0
- package/dist/commands/commitments/receipt.js +16 -2
- package/dist/commands/commitments/remediation-proposal.js +6 -2
- package/dist/commands/commitments/remediation-proposals.js +5 -1
- package/dist/commands/commitments/restore.d.ts +25 -0
- package/dist/commands/commitments/restore.js +46 -0
- package/dist/commands/commitments/show.js +32 -0
- package/dist/commands/commitments/trigger-status.js +33 -0
- package/dist/commands/commitments/update.d.ts +1 -0
- package/dist/commands/commitments/update.js +31 -5
- package/dist/commands/commitments/wake-capabilities.d.ts +0 -1
- package/dist/commands/commitments/wake-capabilities.js +8 -4
- package/dist/commands/enterprise/budgets/create.d.ts +11 -0
- package/dist/commands/enterprise/budgets/create.js +27 -0
- package/dist/commands/enterprise/budgets/evaluate.d.ts +17 -0
- package/dist/commands/enterprise/budgets/evaluate.js +32 -0
- package/dist/commands/enterprise/budgets/publish.d.ts +14 -0
- package/dist/commands/enterprise/budgets/publish.js +30 -0
- package/dist/commands/enterprise/contracts/activate.d.ts +14 -0
- package/dist/commands/enterprise/contracts/activate.js +30 -0
- package/dist/commands/enterprise/contracts/create.d.ts +14 -0
- package/dist/commands/enterprise/contracts/create.js +32 -0
- package/dist/commands/enterprise/contracts/issue.d.ts +18 -0
- package/dist/commands/enterprise/contracts/issue.js +34 -0
- package/dist/commands/enterprise/domains/begin.d.ts +20 -0
- package/dist/commands/enterprise/domains/begin.js +39 -0
- package/dist/commands/enterprise/domains/verify.d.ts +14 -0
- package/dist/commands/enterprise/domains/verify.js +32 -0
- package/dist/commands/enterprise/external-config/activate.d.ts +22 -0
- package/dist/commands/enterprise/external-config/activate.js +49 -0
- package/dist/commands/enterprise/external-config/set.d.ts +25 -0
- package/dist/commands/enterprise/external-config/set.js +123 -0
- package/dist/commands/enterprise/external-config/show.d.ts +12 -0
- package/dist/commands/enterprise/external-config/show.js +39 -0
- package/dist/commands/enterprise/external-config/suspend.d.ts +13 -0
- package/dist/commands/enterprise/external-config/suspend.js +41 -0
- package/dist/commands/enterprise/finance/close-snapshot.d.ts +11 -0
- package/dist/commands/enterprise/finance/close-snapshot.js +28 -0
- package/dist/commands/enterprise/finance/record-document.d.ts +18 -0
- package/dist/commands/enterprise/finance/record-document.js +35 -0
- package/dist/commands/enterprise/profile.d.ts +20 -0
- package/dist/commands/enterprise/profile.js +38 -0
- package/dist/commands/login.d.ts +13 -0
- package/dist/commands/login.js +77 -0
- package/dist/commands/machines/dedicated/index.js +3 -0
- package/dist/commands/machines/dedicated/service/apply.d.ts +23 -0
- package/dist/commands/machines/dedicated/service/apply.js +99 -0
- package/dist/commands/machines/dedicated/service/approve.d.ts +23 -0
- package/dist/commands/machines/dedicated/service/approve.js +52 -0
- package/dist/commands/machines/dedicated/service/create.d.ts +55 -0
- package/dist/commands/machines/dedicated/service/create.js +222 -0
- package/dist/commands/machines/dedicated/service/edit.d.ts +30 -0
- package/dist/commands/machines/dedicated/service/edit.js +146 -0
- package/dist/commands/machines/dedicated/service/exec.d.ts +26 -0
- package/dist/commands/machines/dedicated/service/exec.js +92 -0
- package/dist/commands/machines/dedicated/service/export.d.ts +29 -0
- package/dist/commands/machines/dedicated/service/export.js +106 -0
- package/dist/commands/machines/dedicated/service/index.d.ts +14 -0
- package/dist/commands/machines/dedicated/service/index.js +49 -0
- package/dist/commands/machines/dedicated/service/logs.d.ts +33 -0
- package/dist/commands/machines/dedicated/service/logs.js +148 -0
- package/dist/commands/machines/dedicated/service/ls.d.ts +20 -0
- package/dist/commands/machines/dedicated/service/ls.js +80 -0
- package/dist/commands/machines/dedicated/service/restart.d.ts +14 -0
- package/dist/commands/machines/dedicated/service/restart.js +29 -0
- package/dist/commands/machines/dedicated/service/rm.d.ts +20 -0
- package/dist/commands/machines/dedicated/service/rm.js +33 -0
- package/dist/commands/machines/dedicated/service/secret/index.d.ts +14 -0
- package/dist/commands/machines/dedicated/service/secret/index.js +25 -0
- package/dist/commands/machines/dedicated/service/secret/rm.d.ts +22 -0
- package/dist/commands/machines/dedicated/service/secret/rm.js +54 -0
- package/dist/commands/machines/dedicated/service/secret/set.d.ts +22 -0
- package/dist/commands/machines/dedicated/service/secret/set.js +93 -0
- package/dist/commands/machines/dedicated/service/start.d.ts +14 -0
- package/dist/commands/machines/dedicated/service/start.js +29 -0
- package/dist/commands/machines/dedicated/service/status.d.ts +21 -0
- package/dist/commands/machines/dedicated/service/status.js +85 -0
- package/dist/commands/machines/dedicated/service/stop.d.ts +20 -0
- package/dist/commands/machines/dedicated/service/stop.js +35 -0
- package/dist/commands/machines/dedicated/service/top.d.ts +22 -0
- package/dist/commands/machines/dedicated/service/top.js +120 -0
- package/dist/commands/machines/dedicated/sign-in-status.d.ts +22 -0
- package/dist/commands/machines/dedicated/sign-in-status.js +79 -0
- package/dist/commands/machines/dedicated/ssh-config.d.ts +28 -0
- package/dist/commands/machines/dedicated/ssh-config.js +66 -0
- package/dist/commands/machines/dedicated/ssh-proxy.d.ts +22 -0
- package/dist/commands/machines/dedicated/ssh-proxy.js +62 -0
- package/dist/commands/machines/dedicated/ssh.d.ts +29 -0
- package/dist/commands/machines/dedicated/ssh.js +121 -0
- package/dist/commands/native/allow-all.d.ts +14 -0
- package/dist/commands/native/allow-all.js +25 -0
- package/dist/commands/native/allow.d.ts +24 -0
- package/dist/commands/native/allow.js +45 -0
- package/dist/commands/native/deny.d.ts +24 -0
- package/dist/commands/native/deny.js +43 -0
- package/dist/commands/native/disable.d.ts +17 -0
- package/dist/commands/native/disable.js +28 -0
- package/dist/commands/native/enable.d.ts +12 -0
- package/dist/commands/native/enable.js +29 -0
- package/dist/commands/native/list-apps.d.ts +19 -0
- package/dist/commands/native/list-apps.js +30 -0
- package/dist/commands/native/restrict-to.d.ts +22 -0
- package/dist/commands/native/restrict-to.js +41 -0
- package/dist/commands/native/status.d.ts +19 -0
- package/dist/commands/native/status.js +33 -0
- package/dist/commands/native.d.ts +39 -0
- package/dist/commands/native.js +76 -0
- package/dist/commands/service/index.d.ts +14 -0
- package/dist/commands/service/index.js +31 -0
- package/dist/commands/service/logs.d.ts +18 -0
- package/dist/commands/service/logs.js +53 -0
- package/dist/commands/service/ls.d.ts +13 -0
- package/dist/commands/service/ls.js +48 -0
- package/dist/commands/service/propose.d.ts +34 -0
- package/dist/commands/service/propose.js +113 -0
- package/dist/commands/service/status.d.ts +17 -0
- package/dist/commands/service/status.js +56 -0
- package/dist/commands/spaces/auto-executor/runs.d.ts +13 -0
- package/dist/commands/spaces/auto-executor/runs.js +56 -0
- package/dist/commands/store/governance/artifacts.d.ts +25 -0
- package/dist/commands/store/governance/artifacts.js +55 -0
- package/dist/commands/store/governance/assign.d.ts +16 -0
- package/dist/commands/store/governance/assign.js +54 -0
- package/dist/commands/store/governance/create-license.d.ts +18 -0
- package/dist/commands/store/governance/create-license.js +35 -0
- package/dist/commands/store/governance/install-plan.d.ts +25 -0
- package/dist/commands/store/governance/install-plan.js +56 -0
- package/dist/commands/store/governance/license-policy.d.ts +30 -0
- package/dist/commands/store/governance/license-policy.js +69 -0
- package/dist/commands/store/governance/policy.d.ts +25 -0
- package/dist/commands/store/governance/policy.js +65 -0
- package/dist/commands/store/governance/publish-release.d.ts +17 -0
- package/dist/commands/store/governance/publish-release.js +34 -0
- package/dist/commands/tasks/deliverable/add.d.ts +15 -0
- package/dist/commands/tasks/deliverable/add.js +174 -0
- package/dist/commands/tasks/deliverable/archive.d.ts +35 -0
- package/dist/commands/tasks/deliverable/archive.js +205 -0
- package/dist/commands/tasks/labels/detach.js +3 -0
- package/dist/commands/tasks/labels/suggest.d.ts +24 -0
- package/dist/commands/tasks/labels/suggest.js +69 -0
- package/dist/commands/tasks/list.d.ts +16 -2
- package/dist/commands/tasks/list.js +100 -24
- package/dist/commands/tasks/show.d.ts +23 -0
- package/dist/commands/tasks/show.js +51 -1
- package/dist/commands/wiki/rm.d.ts +22 -1
- package/dist/commands/wiki/rm.js +52 -3
- package/dist/lib/agentic-stream.js +10 -2
- package/dist/lib/balance.d.ts +6 -1
- package/dist/lib/balance.js +15 -0
- package/dist/lib/commerce-command.d.ts +48 -0
- package/dist/lib/commerce-command.js +70 -0
- package/dist/lib/commerce.d.ts +93 -0
- package/dist/lib/commerce.js +129 -0
- package/dist/lib/commitment-product.js +1 -1
- package/dist/lib/commitments.d.ts +202 -29
- package/dist/lib/commitments.js +300 -33
- package/dist/lib/daemon-installer.d.ts +6 -5
- package/dist/lib/daemon-installer.js +4 -17
- package/dist/lib/dedicated-lease-command.js +10 -0
- package/dist/lib/dedicated-machines.d.ts +37 -0
- package/dist/lib/dedicated-machines.js +47 -0
- package/dist/lib/dedicated-service-command.d.ts +60 -0
- package/dist/lib/dedicated-service-command.js +182 -0
- package/dist/lib/dedicated-service.d.ts +342 -0
- package/dist/lib/dedicated-service.js +741 -0
- package/dist/lib/dedicated-ssh.d.ts +136 -0
- package/dist/lib/dedicated-ssh.js +387 -0
- package/dist/lib/delegated-cli.js +51 -13
- package/dist/lib/enterprise-commerce.d.ts +67 -0
- package/dist/lib/enterprise-commerce.js +112 -0
- package/dist/lib/enterprise-external-configuration.d.ts +60 -0
- package/dist/lib/enterprise-external-configuration.js +58 -0
- package/dist/lib/first-party-harness.js +17 -3
- package/dist/lib/governed-store.d.ts +110 -0
- package/dist/lib/governed-store.js +79 -0
- package/dist/lib/guest-service-command.d.ts +16 -0
- package/dist/lib/guest-service-command.js +32 -0
- package/dist/lib/guest-service.d.ts +67 -0
- package/dist/lib/guest-service.js +112 -0
- package/dist/lib/prompt.d.ts +1 -0
- package/dist/lib/prompt.js +1 -0
- package/dist/lib/task-asset-upload.d.ts +20 -0
- package/dist/lib/task-asset-upload.js +30 -8
- package/dist/lib/task-extras.d.ts +23 -0
- package/dist/lib/task-extras.js +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.d.ts +9 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.js +10 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +16 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +21 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +66 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +24 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.d.ts +9 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.js +10 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +16 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +20 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +64 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +24 -2
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +5060 -4827
- package/oclif.manifest.json +35194 -29157
- package/package.json +43 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { io, type Socket } from 'socket.io-client';
|
|
2
|
+
/**
|
|
3
|
+
* Owner SSH to a Dedicated Runtime — SSH protocol over the admitted relay.
|
|
4
|
+
*
|
|
5
|
+
* Design: docs/architecture/dedicated-runtime-ssh-access-2026-09-21.md.
|
|
6
|
+
*
|
|
7
|
+
* This module is the ProxyCommand end of the arrangement. `ssh` speaks its
|
|
8
|
+
* binary protocol to a ProxyCommand's stdio; `runSshProxy` connects the CLI's
|
|
9
|
+
* authenticated socket, asks the API for a channel on the lease
|
|
10
|
+
* (`dedicated:ssh:start`, carrying an ephemeral public key the daemon admits
|
|
11
|
+
* for this one channel), and then pipes bytes. Nothing here authenticates the
|
|
12
|
+
* MACHINE — the API admission does — and nothing here authenticates the
|
|
13
|
+
* CLIENT to sshd — the daemon's admission of the public key does, for exactly
|
|
14
|
+
* as long as the channel lives.
|
|
15
|
+
*
|
|
16
|
+
* Per-device files under ~/.skrr/ssh/ (one identity, one pin file, every
|
|
17
|
+
* lease — the config block is a wildcard `Host rtl_*.skrr`, so setup is
|
|
18
|
+
* once-ever, not once-per-lease):
|
|
19
|
+
*
|
|
20
|
+
* id_ed25519(.pub) the identity this device offers. It is NOT "the key to
|
|
21
|
+
* the machine" — it opens nothing unless the daemon
|
|
22
|
+
* admitted it for a live channel, and the daemon revokes
|
|
23
|
+
* it the moment the channel ends. Losing it loses nothing
|
|
24
|
+
* that was not already a logged-in CLI.
|
|
25
|
+
* known_hosts the host's line is rewritten on every connect from the
|
|
26
|
+
* host keys the daemon just read off THIS instance's
|
|
27
|
+
* /etc/ssh — carried inside the admitted, generation-
|
|
28
|
+
* fenced channel, so the usual first-connect TOFU
|
|
29
|
+
* question never exists, and a replaced machine (new host
|
|
30
|
+
* keys, same workspace) is a silent update rather than a
|
|
31
|
+
* MITM warning.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SSH_HOST_SUFFIX = ".skrr";
|
|
34
|
+
export declare const SSH_USER = "skrr-workload";
|
|
35
|
+
export declare const SSH_START_TIMEOUT_MS = 30000;
|
|
36
|
+
/** The managed block's markers in ~/.ssh/config — one bounded, owned region. */
|
|
37
|
+
export declare const SSH_CONFIG_BEGIN = "# >>> skrr dedicated runtime >>>";
|
|
38
|
+
export declare const SSH_CONFIG_END = "# <<< skrr dedicated runtime <<<";
|
|
39
|
+
export declare class DedicatedSshError extends Error {
|
|
40
|
+
readonly code: string;
|
|
41
|
+
constructor(message: string, code: string);
|
|
42
|
+
}
|
|
43
|
+
/** The lease a ProxyCommand host names: `rtl_x.skrr` or a bare `rtl_x`. */
|
|
44
|
+
export declare function leaseIdForSshHost(host: string): string | null;
|
|
45
|
+
export declare function sshDir(home?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* The identity key this device offers to any lease, created on first use.
|
|
48
|
+
* 0600 in a 0700 directory; `ssh-keygen` is on every machine that has `ssh`,
|
|
49
|
+
* which is every machine this feature runs on. Per-device by design (§8): the
|
|
50
|
+
* grant is the channel admission, not the key — one keypair for the device is
|
|
51
|
+
* the whole surface, so there is nothing per-lease to rotate or lose.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ensureSshIdentity(home?: string): {
|
|
54
|
+
privateKey: string;
|
|
55
|
+
publicKey: string;
|
|
56
|
+
keyType: string;
|
|
57
|
+
keyBlob: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Rewrite `host`'s lines in the shared known_hosts from keys carried by the
|
|
61
|
+
* admitted channel. Other leases' pins are kept — each connect replaces only
|
|
62
|
+
* its own host's rows, atomically. Returns false when the daemon reported no
|
|
63
|
+
* keys: the caller proceeds, and StrictHostKeyChecking=yes surfaces the gap
|
|
64
|
+
* honestly rather than silently accepting a key nobody attested.
|
|
65
|
+
*/
|
|
66
|
+
export declare function writeKnownHosts(host: string, hostKeys: {
|
|
67
|
+
type: string;
|
|
68
|
+
key: string;
|
|
69
|
+
}[] | undefined, home?: string): boolean;
|
|
70
|
+
export declare function knownHostsPath(home?: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The ProxyCommand string OpenSSH runs, quoted for the shell OpenSSH applies
|
|
73
|
+
* to it: `/bin/sh -c` on POSIX (single quotes), `cmd.exe /c` on Windows
|
|
74
|
+
* (double quotes). `argv[1]` is the CLI's entry script (bin/run.js), so this
|
|
75
|
+
* re-invokes whatever binary the user called — installed or in dev.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sshProxyCommand(platform?: NodeJS.Platform): string;
|
|
78
|
+
/**
|
|
79
|
+
* The managed `Host` block — one wildcard stanza covers every lease, so the
|
|
80
|
+
* user installs it once and `ssh rtl_<id>.skrr` (and scp, rsync, Remote-SSH)
|
|
81
|
+
* works for every machine they ever lease.
|
|
82
|
+
*
|
|
83
|
+
* `StrictHostKeyChecking yes`, deliberately not accept-new: the host key is
|
|
84
|
+
* pinned by the proxy from the admitted channel BEFORE the handshake reads
|
|
85
|
+
* the file, so "yes" costs nothing and an unattested key can never be
|
|
86
|
+
* silently accepted. `IdentitiesOnly` keeps the user's real agent keys from
|
|
87
|
+
* being offered — they are never admitted, and offering them would just
|
|
88
|
+
* spend `MaxAuthTries`. Paths are absolute so the block stays correct even
|
|
89
|
+
* if the user's HOME moves.
|
|
90
|
+
*/
|
|
91
|
+
export declare function sshConfigBlock(home?: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Install the managed block into ~/.ssh/config between the skrr markers — a
|
|
94
|
+
* bounded, owned region of a file the user owns (the control-surface rule).
|
|
95
|
+
*
|
|
96
|
+
* Refuses — rather than edits — when the markers exist but cannot bound a
|
|
97
|
+
* region: one marker alone, out of order, or duplicated (a hand-edited file,
|
|
98
|
+
* or another tool already using these strings). Guessing at a corrupted
|
|
99
|
+
* region risks writing over the user's own Host blocks; a refusal names
|
|
100
|
+
* exactly what to fix.
|
|
101
|
+
*/
|
|
102
|
+
export declare function installSshConfigBlock(home?: string): {
|
|
103
|
+
configPath: string;
|
|
104
|
+
changed: boolean;
|
|
105
|
+
};
|
|
106
|
+
export interface SshStartAck {
|
|
107
|
+
channelId?: string;
|
|
108
|
+
ok?: boolean;
|
|
109
|
+
error?: string;
|
|
110
|
+
code?: string;
|
|
111
|
+
hostKeys?: {
|
|
112
|
+
type: string;
|
|
113
|
+
key: string;
|
|
114
|
+
}[];
|
|
115
|
+
}
|
|
116
|
+
export interface SshProxyOptions {
|
|
117
|
+
baseURL: string;
|
|
118
|
+
token: string;
|
|
119
|
+
leaseId: string;
|
|
120
|
+
input: NodeJS.ReadStream;
|
|
121
|
+
output: NodeJS.WriteStream;
|
|
122
|
+
/** Diagnostics; never stdout — ssh reads stdout as protocol bytes. */
|
|
123
|
+
errorOutput: NodeJS.WritableStream;
|
|
124
|
+
ttlSeconds?: number;
|
|
125
|
+
/** Injected for tests. */
|
|
126
|
+
connect?: (url: string, opts: Parameters<typeof io>[1]) => Socket;
|
|
127
|
+
/** Injected for tests. */
|
|
128
|
+
writeKnownHosts?: typeof writeKnownHosts;
|
|
129
|
+
}
|
|
130
|
+
export declare function describeSshStartRefusal(ack: SshStartAck): string;
|
|
131
|
+
/**
|
|
132
|
+
* Bridge stdio to one SSH channel for as long as it lives. Used as
|
|
133
|
+
* `ProxyCommand`: ssh writes the protocol to our stdin and reads it from our
|
|
134
|
+
* stdout. Resolves when the channel ends.
|
|
135
|
+
*/
|
|
136
|
+
export declare function runSshProxy(options: SshProxyOptions): Promise<number>;
|
|
@@ -0,0 +1,387 @@
|
|
|
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.DedicatedSshError = exports.SSH_CONFIG_END = exports.SSH_CONFIG_BEGIN = exports.SSH_START_TIMEOUT_MS = exports.SSH_USER = exports.SSH_HOST_SUFFIX = void 0;
|
|
7
|
+
exports.leaseIdForSshHost = leaseIdForSshHost;
|
|
8
|
+
exports.sshDir = sshDir;
|
|
9
|
+
exports.ensureSshIdentity = ensureSshIdentity;
|
|
10
|
+
exports.writeKnownHosts = writeKnownHosts;
|
|
11
|
+
exports.knownHostsPath = knownHostsPath;
|
|
12
|
+
exports.sshProxyCommand = sshProxyCommand;
|
|
13
|
+
exports.sshConfigBlock = sshConfigBlock;
|
|
14
|
+
exports.installSshConfigBlock = installSshConfigBlock;
|
|
15
|
+
exports.describeSshStartRefusal = describeSshStartRefusal;
|
|
16
|
+
exports.runSshProxy = runSshProxy;
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const node_child_process_1 = require("node:child_process");
|
|
19
|
+
const node_fs_1 = require("node:fs");
|
|
20
|
+
const node_os_1 = require("node:os");
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
23
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
24
|
+
const agentic_stream_1 = require("./agentic-stream");
|
|
25
|
+
/**
|
|
26
|
+
* Owner SSH to a Dedicated Runtime — SSH protocol over the admitted relay.
|
|
27
|
+
*
|
|
28
|
+
* Design: docs/architecture/dedicated-runtime-ssh-access-2026-09-21.md.
|
|
29
|
+
*
|
|
30
|
+
* This module is the ProxyCommand end of the arrangement. `ssh` speaks its
|
|
31
|
+
* binary protocol to a ProxyCommand's stdio; `runSshProxy` connects the CLI's
|
|
32
|
+
* authenticated socket, asks the API for a channel on the lease
|
|
33
|
+
* (`dedicated:ssh:start`, carrying an ephemeral public key the daemon admits
|
|
34
|
+
* for this one channel), and then pipes bytes. Nothing here authenticates the
|
|
35
|
+
* MACHINE — the API admission does — and nothing here authenticates the
|
|
36
|
+
* CLIENT to sshd — the daemon's admission of the public key does, for exactly
|
|
37
|
+
* as long as the channel lives.
|
|
38
|
+
*
|
|
39
|
+
* Per-device files under ~/.skrr/ssh/ (one identity, one pin file, every
|
|
40
|
+
* lease — the config block is a wildcard `Host rtl_*.skrr`, so setup is
|
|
41
|
+
* once-ever, not once-per-lease):
|
|
42
|
+
*
|
|
43
|
+
* id_ed25519(.pub) the identity this device offers. It is NOT "the key to
|
|
44
|
+
* the machine" — it opens nothing unless the daemon
|
|
45
|
+
* admitted it for a live channel, and the daemon revokes
|
|
46
|
+
* it the moment the channel ends. Losing it loses nothing
|
|
47
|
+
* that was not already a logged-in CLI.
|
|
48
|
+
* known_hosts the host's line is rewritten on every connect from the
|
|
49
|
+
* host keys the daemon just read off THIS instance's
|
|
50
|
+
* /etc/ssh — carried inside the admitted, generation-
|
|
51
|
+
* fenced channel, so the usual first-connect TOFU
|
|
52
|
+
* question never exists, and a replaced machine (new host
|
|
53
|
+
* keys, same workspace) is a silent update rather than a
|
|
54
|
+
* MITM warning.
|
|
55
|
+
*/
|
|
56
|
+
exports.SSH_HOST_SUFFIX = '.skrr';
|
|
57
|
+
exports.SSH_USER = 'skrr-workload';
|
|
58
|
+
exports.SSH_START_TIMEOUT_MS = 30_000;
|
|
59
|
+
/** The managed block's markers in ~/.ssh/config — one bounded, owned region. */
|
|
60
|
+
exports.SSH_CONFIG_BEGIN = '# >>> skrr dedicated runtime >>>';
|
|
61
|
+
exports.SSH_CONFIG_END = '# <<< skrr dedicated runtime <<<';
|
|
62
|
+
const LEASE_ID_PATTERN = /^rtl_[a-z0-9]{8,64}$/;
|
|
63
|
+
const HOSTKEY_TYPES = new Set([
|
|
64
|
+
'ssh-ed25519',
|
|
65
|
+
'ssh-rsa',
|
|
66
|
+
'ecdsa-sha2-nistp256',
|
|
67
|
+
'ecdsa-sha2-nistp384',
|
|
68
|
+
'ecdsa-sha2-nistp521',
|
|
69
|
+
]);
|
|
70
|
+
class DedicatedSshError extends Error {
|
|
71
|
+
code;
|
|
72
|
+
constructor(message, code) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = 'DedicatedSshError';
|
|
75
|
+
this.code = code;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.DedicatedSshError = DedicatedSshError;
|
|
79
|
+
/** The lease a ProxyCommand host names: `rtl_x.skrr` or a bare `rtl_x`. */
|
|
80
|
+
function leaseIdForSshHost(host) {
|
|
81
|
+
const value = String(host || '').trim();
|
|
82
|
+
const bare = value.endsWith(exports.SSH_HOST_SUFFIX) ? value.slice(0, -exports.SSH_HOST_SUFFIX.length) : value;
|
|
83
|
+
return LEASE_ID_PATTERN.test(bare) ? bare : null;
|
|
84
|
+
}
|
|
85
|
+
function sshDir(home = (0, node_os_1.homedir)()) {
|
|
86
|
+
return node_path_1.default.join(home, '.skrr', 'ssh');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The identity key this device offers to any lease, created on first use.
|
|
90
|
+
* 0600 in a 0700 directory; `ssh-keygen` is on every machine that has `ssh`,
|
|
91
|
+
* which is every machine this feature runs on. Per-device by design (§8): the
|
|
92
|
+
* grant is the channel admission, not the key — one keypair for the device is
|
|
93
|
+
* the whole surface, so there is nothing per-lease to rotate or lose.
|
|
94
|
+
*/
|
|
95
|
+
function ensureSshIdentity(home = (0, node_os_1.homedir)()) {
|
|
96
|
+
const dir = sshDir(home);
|
|
97
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
|
|
98
|
+
(0, node_fs_1.chmodSync)(dir, 0o700);
|
|
99
|
+
const privateKey = node_path_1.default.join(dir, 'id_ed25519');
|
|
100
|
+
const publicKeyPath = `${privateKey}.pub`;
|
|
101
|
+
if (!(0, node_fs_1.existsSync)(privateKey) || !(0, node_fs_1.existsSync)(publicKeyPath)) {
|
|
102
|
+
try {
|
|
103
|
+
(0, node_child_process_1.execFileSync)('ssh-keygen', [
|
|
104
|
+
'-q',
|
|
105
|
+
'-t',
|
|
106
|
+
'ed25519',
|
|
107
|
+
'-N',
|
|
108
|
+
'',
|
|
109
|
+
'-C',
|
|
110
|
+
'skrr-ssh-device',
|
|
111
|
+
'-f',
|
|
112
|
+
privateKey,
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new DedicatedSshError(`could not create the SSH identity — is \`ssh-keygen\` installed? (${error instanceof Error ? error.message : error})`, 'SSH_KEYGEN_FAILED');
|
|
117
|
+
}
|
|
118
|
+
(0, node_fs_1.chmodSync)(privateKey, 0o600);
|
|
119
|
+
(0, node_fs_1.chmodSync)(publicKeyPath, 0o644);
|
|
120
|
+
}
|
|
121
|
+
const line = (0, node_fs_1.readFileSync)(publicKeyPath, 'utf8').trim();
|
|
122
|
+
const [keyType, keyBlob] = line.split(/\s+/);
|
|
123
|
+
if (!keyType || !keyBlob) {
|
|
124
|
+
throw new DedicatedSshError(`could not read ${publicKeyPath}`, 'SSH_KEY_UNREADABLE');
|
|
125
|
+
}
|
|
126
|
+
return { privateKey, publicKey: publicKeyPath, keyType, keyBlob };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Rewrite `host`'s lines in the shared known_hosts from keys carried by the
|
|
130
|
+
* admitted channel. Other leases' pins are kept — each connect replaces only
|
|
131
|
+
* its own host's rows, atomically. Returns false when the daemon reported no
|
|
132
|
+
* keys: the caller proceeds, and StrictHostKeyChecking=yes surfaces the gap
|
|
133
|
+
* honestly rather than silently accepting a key nobody attested.
|
|
134
|
+
*/
|
|
135
|
+
function writeKnownHosts(host, hostKeys, home = (0, node_os_1.homedir)()) {
|
|
136
|
+
if (!Array.isArray(hostKeys) || hostKeys.length === 0)
|
|
137
|
+
return false;
|
|
138
|
+
const dir = sshDir(home);
|
|
139
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
|
|
140
|
+
(0, node_fs_1.chmodSync)(dir, 0o700);
|
|
141
|
+
const lines = hostKeys
|
|
142
|
+
.filter((k) => k && HOSTKEY_TYPES.has(k.type) && /^[A-Za-z0-9+/]+=*$/.test(k.key))
|
|
143
|
+
.map((k) => `${host} ${k.type} ${k.key}`);
|
|
144
|
+
if (lines.length === 0)
|
|
145
|
+
return false;
|
|
146
|
+
const target = node_path_1.default.join(dir, 'known_hosts');
|
|
147
|
+
let existing = [];
|
|
148
|
+
try {
|
|
149
|
+
existing = (0, node_fs_1.readFileSync)(target, 'utf8')
|
|
150
|
+
.split('\n')
|
|
151
|
+
.filter((l) => l.trim().length > 0);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* first connect */
|
|
155
|
+
}
|
|
156
|
+
const kept = existing.filter((l) => l.split(/\s+/)[0] !== host);
|
|
157
|
+
const tmp = `${target}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
158
|
+
(0, node_fs_1.writeFileSync)(tmp, `${[...kept, ...lines].join('\n')}\n`, { mode: 0o600 });
|
|
159
|
+
(0, node_fs_1.renameSync)(tmp, target);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function knownHostsPath(home = (0, node_os_1.homedir)()) {
|
|
163
|
+
return node_path_1.default.join(sshDir(home), 'known_hosts');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The ProxyCommand string OpenSSH runs, quoted for the shell OpenSSH applies
|
|
167
|
+
* to it: `/bin/sh -c` on POSIX (single quotes), `cmd.exe /c` on Windows
|
|
168
|
+
* (double quotes). `argv[1]` is the CLI's entry script (bin/run.js), so this
|
|
169
|
+
* re-invokes whatever binary the user called — installed or in dev.
|
|
170
|
+
*/
|
|
171
|
+
function sshProxyCommand(platform = process.platform) {
|
|
172
|
+
// POSIX: sh single-quotes — literal except ', which ends the quote and is
|
|
173
|
+
// re-entered as '\''. Windows: OpenSSH there hands the command to
|
|
174
|
+
// `cmd.exe /c`, where ' is not a quote at all — double-quote each word.
|
|
175
|
+
// Backslashes stay literal (cmd does not treat \ as an escape inside
|
|
176
|
+
// quotes), and " cannot appear — Windows forbids it in paths.
|
|
177
|
+
const quote = platform === 'win32'
|
|
178
|
+
? (w) => `"${w}"`
|
|
179
|
+
: (w) => `'${w.split("'").join(`'\\''`)}'`;
|
|
180
|
+
const words = [process.execPath, process.argv[1]]
|
|
181
|
+
.filter((w) => typeof w === 'string' && w.length > 0)
|
|
182
|
+
.map(quote);
|
|
183
|
+
return `${words.join(' ')} machines dedicated ssh-proxy %h`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The managed `Host` block — one wildcard stanza covers every lease, so the
|
|
187
|
+
* user installs it once and `ssh rtl_<id>.skrr` (and scp, rsync, Remote-SSH)
|
|
188
|
+
* works for every machine they ever lease.
|
|
189
|
+
*
|
|
190
|
+
* `StrictHostKeyChecking yes`, deliberately not accept-new: the host key is
|
|
191
|
+
* pinned by the proxy from the admitted channel BEFORE the handshake reads
|
|
192
|
+
* the file, so "yes" costs nothing and an unattested key can never be
|
|
193
|
+
* silently accepted. `IdentitiesOnly` keeps the user's real agent keys from
|
|
194
|
+
* being offered — they are never admitted, and offering them would just
|
|
195
|
+
* spend `MaxAuthTries`. Paths are absolute so the block stays correct even
|
|
196
|
+
* if the user's HOME moves.
|
|
197
|
+
*/
|
|
198
|
+
function sshConfigBlock(home = (0, node_os_1.homedir)()) {
|
|
199
|
+
const dir = sshDir(home);
|
|
200
|
+
return [
|
|
201
|
+
`Host rtl_*${exports.SSH_HOST_SUFFIX}`,
|
|
202
|
+
` User ${exports.SSH_USER}`,
|
|
203
|
+
` IdentityFile ${node_path_1.default.join(dir, 'id_ed25519')}`,
|
|
204
|
+
` IdentitiesOnly yes`,
|
|
205
|
+
` ProxyCommand ${sshProxyCommand()}`,
|
|
206
|
+
` UserKnownHostsFile ${node_path_1.default.join(dir, 'known_hosts')}`,
|
|
207
|
+
` StrictHostKeyChecking yes`,
|
|
208
|
+
` ServerAliveInterval 30`,
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Install the managed block into ~/.ssh/config between the skrr markers — a
|
|
213
|
+
* bounded, owned region of a file the user owns (the control-surface rule).
|
|
214
|
+
*
|
|
215
|
+
* Refuses — rather than edits — when the markers exist but cannot bound a
|
|
216
|
+
* region: one marker alone, out of order, or duplicated (a hand-edited file,
|
|
217
|
+
* or another tool already using these strings). Guessing at a corrupted
|
|
218
|
+
* region risks writing over the user's own Host blocks; a refusal names
|
|
219
|
+
* exactly what to fix.
|
|
220
|
+
*/
|
|
221
|
+
function installSshConfigBlock(home = (0, node_os_1.homedir)()) {
|
|
222
|
+
const configPath = node_path_1.default.join(home, '.ssh', 'config');
|
|
223
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
224
|
+
(0, node_fs_1.chmodSync)(node_path_1.default.dirname(configPath), 0o700);
|
|
225
|
+
const block = `${exports.SSH_CONFIG_BEGIN}\n${sshConfigBlock(home)}\n${exports.SSH_CONFIG_END}`;
|
|
226
|
+
let text = '';
|
|
227
|
+
try {
|
|
228
|
+
text = (0, node_fs_1.readFileSync)(configPath, 'utf8');
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error.code !== 'ENOENT')
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
const firstBegin = text.indexOf(exports.SSH_CONFIG_BEGIN);
|
|
235
|
+
const firstEnd = text.indexOf(exports.SSH_CONFIG_END);
|
|
236
|
+
const malformed = firstBegin === -1 ||
|
|
237
|
+
firstEnd === -1 ||
|
|
238
|
+
firstEnd < firstBegin ||
|
|
239
|
+
text.indexOf(exports.SSH_CONFIG_BEGIN, firstBegin + 1) !== -1 ||
|
|
240
|
+
text.indexOf(exports.SSH_CONFIG_END, firstEnd + 1) !== -1;
|
|
241
|
+
if (text.includes(exports.SSH_CONFIG_BEGIN) || text.includes(exports.SSH_CONFIG_END)) {
|
|
242
|
+
if (malformed) {
|
|
243
|
+
throw new DedicatedSshError(`${configPath} has a partial or duplicated skrr block and is not safe to edit. Fix it by hand, or remove the lines containing 'skrr dedicated runtime' and run ssh-config again.`, 'SSH_CONFIG_COLLISION');
|
|
244
|
+
}
|
|
245
|
+
const next = text.slice(0, firstBegin) + block + text.slice(firstEnd + exports.SSH_CONFIG_END.length);
|
|
246
|
+
(0, node_fs_1.writeFileSync)(configPath, next, { mode: 0o600 });
|
|
247
|
+
return { configPath, changed: next !== text };
|
|
248
|
+
}
|
|
249
|
+
let separator = '';
|
|
250
|
+
if (text.length > 0)
|
|
251
|
+
separator = text.endsWith('\n\n') ? '' : '\n\n';
|
|
252
|
+
const next = `${text}${separator}${block}\n`;
|
|
253
|
+
(0, node_fs_1.writeFileSync)(configPath, next, { mode: 0o600 });
|
|
254
|
+
return { configPath, changed: true };
|
|
255
|
+
}
|
|
256
|
+
function describeSshStartRefusal(ack) {
|
|
257
|
+
const code = ack.code || '';
|
|
258
|
+
if (code === 'DEDICATED_RUNTIME_SSH_UNSUPPORTED') {
|
|
259
|
+
return (ack.error ||
|
|
260
|
+
'This machine’s image does not carry SSH support yet. Update it with `skrr machines dedicated update-image <lease-id> --wait`.');
|
|
261
|
+
}
|
|
262
|
+
if (code === 'dedicated_runtime_quiesced') {
|
|
263
|
+
return 'The machine is checkpointing its workspace right now; try again in a moment.';
|
|
264
|
+
}
|
|
265
|
+
if (code === 'DEDICATED_RUNTIME_GENERATION_STALE') {
|
|
266
|
+
return 'The machine was replaced since its daemon registered. Wait a minute and try again.';
|
|
267
|
+
}
|
|
268
|
+
if (code === 'DEDICATED_RUNTIME_NOT_ASSIGNABLE' || code === 'not_found' || code === 'offline') {
|
|
269
|
+
return 'The machine is not running or its daemon is not connected. Check it with `skrr machines dedicated show <lease-id>`.';
|
|
270
|
+
}
|
|
271
|
+
return ack.error ? `SSH did not start: ${ack.error}` : 'SSH did not start.';
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Bridge stdio to one SSH channel for as long as it lives. Used as
|
|
275
|
+
* `ProxyCommand`: ssh writes the protocol to our stdin and reads it from our
|
|
276
|
+
* stdout. Resolves when the channel ends.
|
|
277
|
+
*/
|
|
278
|
+
function runSshProxy(options) {
|
|
279
|
+
const channelId = `ssh-${(0, node_crypto_1.randomUUID)()}`;
|
|
280
|
+
const connect = options.connect ?? socket_io_client_1.io;
|
|
281
|
+
const { input, output, errorOutput } = options;
|
|
282
|
+
const identity = ensureSshIdentity();
|
|
283
|
+
const writeHosts = options.writeKnownHosts ?? writeKnownHosts;
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
let started = false;
|
|
286
|
+
let finished = false;
|
|
287
|
+
const socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), {
|
|
288
|
+
path: '/ws/agentic',
|
|
289
|
+
auth: {
|
|
290
|
+
token: options.token,
|
|
291
|
+
...(0, data_provider_1.getClientIdentityAuth)(),
|
|
292
|
+
clientPlatform: 'cli',
|
|
293
|
+
clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
294
|
+
},
|
|
295
|
+
transports: ['websocket'],
|
|
296
|
+
// A reconnect cannot resume a channel whose admission line the daemon
|
|
297
|
+
// revoked on socket death — say it ended and let ssh retry fresh.
|
|
298
|
+
reconnection: false,
|
|
299
|
+
});
|
|
300
|
+
const startTimer = setTimeout(() => {
|
|
301
|
+
finish(1, 'The machine did not answer the SSH channel in time. Its daemon may be busy or offline.');
|
|
302
|
+
}, exports.SSH_START_TIMEOUT_MS);
|
|
303
|
+
const onInput = (chunk) => {
|
|
304
|
+
const bytes = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
305
|
+
socket.emit('dedicated:ssh:data', { channelId, data: bytes.toString('base64') });
|
|
306
|
+
};
|
|
307
|
+
function cleanup() {
|
|
308
|
+
clearTimeout(startTimer);
|
|
309
|
+
input.off('data', onInput);
|
|
310
|
+
input.off('end', onInputEnded);
|
|
311
|
+
input.off('close', onInputEnded);
|
|
312
|
+
output.off('error', onOutputError);
|
|
313
|
+
input.pause();
|
|
314
|
+
}
|
|
315
|
+
// The ssh client exiting is visible ONLY as stdin EOF and an EPIPE on
|
|
316
|
+
// stdout — sshd cannot tell the ProxyCommand died until its socket sees
|
|
317
|
+
// the stop or the transport drops. Listen for both so the channel, and
|
|
318
|
+
// the daemon's key admission, end when ssh does instead of at idle
|
|
319
|
+
// timeout.
|
|
320
|
+
const onInputEnded = () => finish(0);
|
|
321
|
+
const onOutputError = () => finish(1);
|
|
322
|
+
input.once('end', onInputEnded);
|
|
323
|
+
input.once('close', onInputEnded);
|
|
324
|
+
output.once('error', onOutputError);
|
|
325
|
+
function finish(code, notice) {
|
|
326
|
+
if (finished)
|
|
327
|
+
return;
|
|
328
|
+
finished = true;
|
|
329
|
+
cleanup();
|
|
330
|
+
if (notice)
|
|
331
|
+
errorOutput.write(`${notice}\n`);
|
|
332
|
+
if (started)
|
|
333
|
+
socket.emit('dedicated:ssh:stop', { channelId });
|
|
334
|
+
socket.disconnect();
|
|
335
|
+
resolve(code);
|
|
336
|
+
}
|
|
337
|
+
socket.on('connect_error', (err) => {
|
|
338
|
+
finish(1, `Could not connect to skrr: ${err.message}`);
|
|
339
|
+
});
|
|
340
|
+
socket.on('connect', () => {
|
|
341
|
+
socket.emit('dedicated:ssh:start', {
|
|
342
|
+
leaseId: options.leaseId,
|
|
343
|
+
channelId,
|
|
344
|
+
keyType: identity.keyType,
|
|
345
|
+
keyBlob: identity.keyBlob,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
socket.on('dedicated:ssh:start:ack', (ack) => {
|
|
349
|
+
if (!ack || ack.channelId !== channelId || started || finished)
|
|
350
|
+
return;
|
|
351
|
+
if (!ack.ok) {
|
|
352
|
+
finish(1, describeSshStartRefusal(ack));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
started = true;
|
|
356
|
+
clearTimeout(startTimer);
|
|
357
|
+
// The instance's keys, attested by the channel — write them before the
|
|
358
|
+
// first protocol byte flows so ssh's host-key check reads the new file.
|
|
359
|
+
// `host` is what ssh asks about (`%h`), so the pin goes under the alias;
|
|
360
|
+
// a daemon that reported no keys becomes an honest StrictHostKeyChecking
|
|
361
|
+
// refusal, never a silent TOFU.
|
|
362
|
+
const host = `${options.leaseId}${exports.SSH_HOST_SUFFIX}`;
|
|
363
|
+
if (!writeHosts(host, ack.hostKeys)) {
|
|
364
|
+
errorOutput.write('[skrr ssh] warning: the machine reported no host keys; the connection will be refused if none are pinned\n');
|
|
365
|
+
}
|
|
366
|
+
input.resume();
|
|
367
|
+
input.on('data', onInput);
|
|
368
|
+
});
|
|
369
|
+
socket.on('dedicated:ssh:data', (frame) => {
|
|
370
|
+
if (!frame || frame.channelId !== channelId || finished)
|
|
371
|
+
return;
|
|
372
|
+
if (typeof frame.data === 'string' && frame.data.length > 0) {
|
|
373
|
+
output.write(Buffer.from(frame.data, 'base64'));
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
socket.on('dedicated:ssh:closed', (frame) => {
|
|
377
|
+
if (!frame || frame.channelId !== channelId)
|
|
378
|
+
return;
|
|
379
|
+
finish(0, frame.reason ? `[ssh channel closed: ${frame.reason}]` : undefined);
|
|
380
|
+
});
|
|
381
|
+
socket.on('disconnect', () => {
|
|
382
|
+
if (finished)
|
|
383
|
+
return;
|
|
384
|
+
finish(started ? 1 : 1, '[lost the connection to skrr; the SSH channel has ended]');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -77,12 +77,12 @@ function resetDelegatedCliCredentialCache() {
|
|
|
77
77
|
_cached = null;
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
* Walk `err.cause` / `err.errors[]` looking for
|
|
80
|
+
* Walk `err.cause` / `err.errors[]` looking for an errno-style `code`.
|
|
81
81
|
* Node's `fetch` throws a `TypeError('fetch failed')` whose `cause` is either
|
|
82
82
|
* a plain `Error` with `code` set, or an `AggregateError` whose `errors[]`
|
|
83
83
|
* carry it (dual-stack retries). Neither shape is visible at the top level.
|
|
84
84
|
*/
|
|
85
|
-
function
|
|
85
|
+
function fetchTransportCode(err) {
|
|
86
86
|
const seen = new Set();
|
|
87
87
|
const stack = [err];
|
|
88
88
|
while (stack.length) {
|
|
@@ -91,15 +91,18 @@ function isConnectionRefusedError(err) {
|
|
|
91
91
|
continue;
|
|
92
92
|
seen.add(cur);
|
|
93
93
|
const anyCur = cur;
|
|
94
|
-
if (anyCur.code === '
|
|
95
|
-
return
|
|
94
|
+
if (typeof anyCur.code === 'string' && anyCur.code.length > 0)
|
|
95
|
+
return anyCur.code;
|
|
96
96
|
if (anyCur.cause !== undefined)
|
|
97
97
|
stack.push(anyCur.cause);
|
|
98
98
|
if (Array.isArray(anyCur.errors))
|
|
99
99
|
for (const e of anyCur.errors)
|
|
100
100
|
stack.push(e);
|
|
101
101
|
}
|
|
102
|
-
return
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function isConnectionRefusedError(err) {
|
|
105
|
+
return fetchTransportCode(err) === 'ECONNREFUSED';
|
|
103
106
|
}
|
|
104
107
|
/**
|
|
105
108
|
* Best-effort probe of the daemon's bootstrap file — the CLI's
|
|
@@ -164,6 +167,35 @@ function composeStaleBrokerMessage(brokerUrl) {
|
|
|
164
167
|
` unset OVERSKY_DELEGATED_CLI_BROKER_URL OVERSKY_DELEGATED_CLI_CAPABILITY OVERSKY_AUTONOMOUS_SESSION\n` +
|
|
165
168
|
`Subsequent \`skrr\` calls will then auto-broker a fresh credential via the live daemon.`);
|
|
166
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Rewrite a transport-level failure (anything that is not ECONNREFUSED — that
|
|
172
|
+
* one means a dead daemon, handled above) into a message that names the real
|
|
173
|
+
* cause and the recovery, instead of the bare `fetch failed` Node surfaces.
|
|
174
|
+
*/
|
|
175
|
+
function composeUnreachableBrokerMessage(brokerUrl, err) {
|
|
176
|
+
const code = fetchTransportCode(err);
|
|
177
|
+
if (code === 'EPERM') {
|
|
178
|
+
// Observed concretely: a Codex-managed session under a seatbelt
|
|
179
|
+
// workspace-write/read-only profile gets EPERM connecting to
|
|
180
|
+
// 127.0.0.1 — seatbelt's `network-outbound` denial covers loopback.
|
|
181
|
+
return (`the OS denied the connection to the delegated CLI broker at ${brokerUrl} (EPERM). ` +
|
|
182
|
+
`This process is inside a sandbox that blocks outbound network — including ` +
|
|
183
|
+
`127.0.0.1 (Codex's seatbelt workspace-write/read-only profiles do this). ` +
|
|
184
|
+
`Re-run with the sandbox's network access enabled, or outside the sandbox. ` +
|
|
185
|
+
`Unsetting the OVERSKY_DELEGATED_CLI_* vars does not help: the same sandbox ` +
|
|
186
|
+
`would block the upstream API as well.`);
|
|
187
|
+
}
|
|
188
|
+
// undici rejects an aborted fetch with a DOMException named 'AbortError',
|
|
189
|
+
// which is NOT an `instanceof Error` in Node — check the name structurally.
|
|
190
|
+
if (err?.name === 'AbortError') {
|
|
191
|
+
return (`the delegated CLI broker at ${brokerUrl} did not answer within 5s. ` +
|
|
192
|
+
`The daemon listener is accepting connections but not responding — it may be ` +
|
|
193
|
+
`wedged; check the daemon and retry.`);
|
|
194
|
+
}
|
|
195
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
196
|
+
return (`the delegated CLI broker at ${brokerUrl} is unreachable: ${detail}` +
|
|
197
|
+
(code ? ` (${code})` : ''));
|
|
198
|
+
}
|
|
167
199
|
/** Fetch a full-permission CLI access token from the local daemon. */
|
|
168
200
|
async function fetchDelegatedCliCredential(env = process.env, options = {}) {
|
|
169
201
|
if (!isDelegatedCliSession(env))
|
|
@@ -197,12 +229,14 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
|
|
|
197
229
|
// from a live daemon: it means the whole loopback listener is gone, so
|
|
198
230
|
// the capability is unrecoverable rather than transiently unavailable.
|
|
199
231
|
// Rewrite the message so `base-command` surfaces recovery text instead
|
|
200
|
-
// of a bare `fetch failed`.
|
|
201
|
-
//
|
|
232
|
+
// of a bare `fetch failed`. Other transport errors keep the original as
|
|
233
|
+
// `cause` but still get a rewritten headline — EPERM (a sandbox denying
|
|
234
|
+
// loopback), a timeout, and a bare `fetch failed` are different problems
|
|
235
|
+
// with different remedies.
|
|
202
236
|
if (isConnectionRefusedError(err)) {
|
|
203
|
-
throw new Error(composeStaleBrokerMessage(brokerUrl));
|
|
237
|
+
throw new Error(composeStaleBrokerMessage(brokerUrl), { cause: err });
|
|
204
238
|
}
|
|
205
|
-
throw err;
|
|
239
|
+
throw new Error(composeUnreachableBrokerMessage(brokerUrl, err), { cause: err });
|
|
206
240
|
}
|
|
207
241
|
if (!res.ok) {
|
|
208
242
|
// A DELIBERATE decline is not a broker failure — WL-5 Step 1.
|
|
@@ -220,10 +254,14 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
|
|
|
220
254
|
// Agent whose daemon cannot rotate its family must surface that rather
|
|
221
255
|
// than quietly degrade, because a degraded credential path is exactly
|
|
222
256
|
// where a human's stored family gets adopted by accident.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
257
|
+
const body = (await res.json().catch(() => null));
|
|
258
|
+
if (res.status === 403 && body?.code === 'DELEGATED_CLI_CAPABILITY_REJECTED')
|
|
259
|
+
return null;
|
|
260
|
+
if (res.status === 503 || body?.code === 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE') {
|
|
261
|
+
throw new Error(`the daemon's delegated CLI broker is alive but cannot mint a credential ` +
|
|
262
|
+
`(HTTP ${res.status}). The daemon cannot reach the auth upstream, holds ` +
|
|
263
|
+
`no daemon bearer, or is inside its mint cool-down — check the daemon ` +
|
|
264
|
+
`log for [delegated-cli]/[task-token] lines and retry.`);
|
|
227
265
|
}
|
|
228
266
|
throw new Error(`Delegated skrr CLI credential broker returned HTTP ${res.status}.`);
|
|
229
267
|
}
|