@volter/supercode-teams 0.2.20

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.
Files changed (79) hide show
  1. package/README.md +119 -0
  2. package/attention.mjs +191 -0
  3. package/bin/fleet.mjs +151 -0
  4. package/bin/teams.mjs +291 -0
  5. package/bin/workspace/args.mjs +66 -0
  6. package/bin/workspace/files.mjs +110 -0
  7. package/bin/workspace/lifecycle.mjs +113 -0
  8. package/bin/workspace/volter-device.mjs +42 -0
  9. package/bin/workspace/volter-login.mjs +185 -0
  10. package/bin/workspace.mjs +726 -0
  11. package/browser-attach.mjs +90 -0
  12. package/client.mjs +212 -0
  13. package/connector/effects.mjs +95 -0
  14. package/connector/index.d.ts +69 -0
  15. package/connector/index.mjs +174 -0
  16. package/connector/native-continuation.d.ts +8 -0
  17. package/connector/native-continuation.mjs +50 -0
  18. package/connector/native-effects.mjs +164 -0
  19. package/context.mjs +34 -0
  20. package/contracts/adapter-map.json +16 -0
  21. package/contracts/index.d.ts +299 -0
  22. package/contracts/index.mjs +37 -0
  23. package/execution.mjs +206 -0
  24. package/fleet.mjs +78 -0
  25. package/index.mjs +12 -0
  26. package/machine-door.mjs +36 -0
  27. package/machine.mjs +1398 -0
  28. package/package.json +80 -0
  29. package/porthole-hook.cjs +137 -0
  30. package/remote/job-exec.py +146 -0
  31. package/remote/wire.py +23 -0
  32. package/server/arc2-transactions.mjs +125 -0
  33. package/server/attachment-relay.mjs +149 -0
  34. package/server/catalog.mjs +7 -0
  35. package/server/continuations.mjs +56 -0
  36. package/server/correlation.d.ts +4 -0
  37. package/server/correlation.mjs +17 -0
  38. package/server/effects-transactions.mjs +119 -0
  39. package/server/effects.mjs +97 -0
  40. package/server/event-routes.mjs +208 -0
  41. package/server/exports.mjs +46 -0
  42. package/server/harness-routes.mjs +248 -0
  43. package/server/index.d.ts +31 -0
  44. package/server/index.mjs +52 -0
  45. package/server/ingest.mjs +24 -0
  46. package/server/node-upgrade.mjs +39 -0
  47. package/server/otlp/collector-config.d.ts +2 -0
  48. package/server/otlp/collector-config.mjs +26 -0
  49. package/server/otlp/decoders/index.d.ts +63 -0
  50. package/server/otlp/decoders/index.mjs +311 -0
  51. package/server/otlp/index.d.ts +4 -0
  52. package/server/otlp/index.mjs +4 -0
  53. package/server/otlp/projection.d.ts +5 -0
  54. package/server/otlp/projection.mjs +17 -0
  55. package/server/otlp/receiver.d.ts +3 -0
  56. package/server/otlp/receiver.mjs +36 -0
  57. package/server/otlp/schema/google/protobuf/any.proto +162 -0
  58. package/server/otlp/schema/google/rpc/status.proto +48 -0
  59. package/server/otlp/schema/manifest.json +22 -0
  60. package/server/otlp/schema/opentelemetry/proto/collector/logs/v1/logs_service.proto +77 -0
  61. package/server/otlp/schema/opentelemetry/proto/collector/metrics/v1/metrics_service.proto +77 -0
  62. package/server/otlp/schema/opentelemetry/proto/collector/trace/v1/trace_service.proto +77 -0
  63. package/server/otlp/schema/opentelemetry/proto/common/v1/common.proto +154 -0
  64. package/server/otlp/schema/opentelemetry/proto/logs/v1/logs.proto +226 -0
  65. package/server/otlp/schema/opentelemetry/proto/metrics/v1/metrics.proto +735 -0
  66. package/server/otlp/schema/opentelemetry/proto/resource/v1/resource.proto +45 -0
  67. package/server/otlp/schema/opentelemetry/proto/trace/v1/trace.proto +359 -0
  68. package/server/otlp/transport.d.ts +10 -0
  69. package/server/otlp/transport.mjs +424 -0
  70. package/server/otlp/wire-codec.mjs +153 -0
  71. package/server/store-core.mjs +139 -0
  72. package/server/store-worker.mjs +23 -0
  73. package/worker/fetch-adapter.mjs +49 -0
  74. package/worker/index.mjs +127 -0
  75. package/worker/websocket-duplex.mjs +29 -0
  76. package/worker/wrangler.toml +38 -0
  77. package/workspace-client.d.ts +75 -0
  78. package/workspace-client.mjs +171 -0
  79. package/workspace-move.mjs +38 -0
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # `@volter/supercode-teams`
2
+
3
+ Harness Teams, the harness's part of Volter Teams, has a server and machines. A **machine daemon**, one per OS user, serves this machine's
4
+ doors: panes, launch, files, ports, attention, titles, a signed `describe`, and `harness.v1`.
5
+ A **Teams server** retains a shared session catalog, membership and access rules, and routes
6
+ callers to enrolled machines. It runs as a Node process (`server/index.mjs`) or as a Cloudflare
7
+ Worker with one Durable Object (`worker/`); both host the same `server/core.mjs`. Existing OpenTelemetry pipelines can publish
8
+ evidence without installing Volter Harness on each developer's machine. Start with the
9
+ [Teams guide](../../docs/guides/teams.md); the design is
10
+ [Teams server, CLI and API](../../docs/plans/teams-server.md).
11
+
12
+ The server uses SQLite and requires Node 22.13 or newer. Package exports: `.` (the daemon,
13
+ channel and client), `/machine`, `/client`, `/admission`, `/fleet`, `/server`,
14
+ `/workspace-client`, `/connector`, `/connector/native-continuation` and `/contracts`. The
15
+ workspace client is browser-safe and takes explicit credentials. Native capture and execution use
16
+ the matching harness SDK and native core; source capabilities and fidelity remain explicit.
17
+ Fresh-session materialization supports Claude Code and Codex with requested `value_lossless`;
18
+ telemetry alone does not establish resumable native history.
19
+
20
+ See the [public client declarations](workspace-client.d.ts), [contracts](contracts/index.d.ts),
21
+ and [implementation specification](../../docs/plans/teams-server-build/README.md).
22
+
23
+ ## The machine daemon
24
+
25
+ `supercode teams machine start` runs it with its local door only; `supercode teams connect` runs
26
+ the same daemon enrolled to the selected Teams context, adding catalog sync and the server link.
27
+ The daemon owns one `supercode harness serve` and the tmux terminal host.
28
+
29
+ Two doors reach it:
30
+
31
+ - **Local**: an owner-only Unix socket, `$SUPERCODE_HOME/teams/machine.sock` (or, when that path
32
+ is too long for a socket address, a per-user 0700 directory under `/tmp`). Whoever can open it
33
+ is this OS user, an operator. The local CLI, the agent inside the machine and a service on the
34
+ same box (RH2's connector) use it.
35
+ - **Remote**: the Teams server. The daemon holds one outbound WebSocket link; the server forwards
36
+ a caller's channel over it after checking the caller's machine grants, and the daemon
37
+ re-derives that admission from its own synced copy of the grants before every door.
38
+
39
+ The machine's custodian holds it as operator. Anyone else holds a **machine grant**:
40
+ `operate` (the whole machine: `harness.v1` and `launch`), or `view`, `observe`, `interact`,
41
+ `control`, `files`, `ports` on named targets (`pane:<key>`, `runtime:<id>`, `root:/path`,
42
+ `port:<pane>/<n>`), or on every target of one kind (`pane:*`, `port:*`). `*` carries only
43
+ `operate` and `view`: a grant that reaches more names the kind it reaches, so it never covers
44
+ a kind another product serves on the same machine. `title.set` needs `control` on its target.
45
+ `observe` on a `root:/path` reads it (stat, list, read, search); `files` also changes it.
46
+
47
+ A workplace can open one pane or one folder for someone who is not a member of the team. A
48
+ server started with `--delegated-issuer ORIGIN` (the Worker's `DELEGATED_ISSUER`) trusts short-lived
49
+ tokens that issuer signs (Ed25519, keys at `ORIGIN/.well-known/jwks.json`, audience this server's
50
+ public origin), each naming a pane (`pane:<key>`) or a folder (`root:/path`) on one machine of one
51
+ team, and `view` or `control`. The hosted viewer at `/view#token=…` opens it; the server turns the
52
+ token into a machine grant on exactly that target (a pane: `observe`, and `control` to type; a
53
+ folder: `observe` to read, `files` to change) that expires with the token, and closes the channel
54
+ then. A page embedding the viewer sends it a fresh token by `postMessage({ type: 'delegated-token',
55
+ token })`, which the viewer asks for with `delegated-token-needed`.
56
+
57
+ `teams publish` puts a pane or a folder of the connected machine (or `--machine NAME`) into a
58
+ workplace Room as a Surface, through the workplace's own door with a session token of yours;
59
+ `teams unpublish SURFACE` retires it:
60
+
61
+ ```sh
62
+ supercode teams publish pane <ctx> --room room_… --workplace https://pilot.runhuman.com --organization acme --token-stdin
63
+ supercode teams publish root ./reports --room room_… --workplace https://pilot.runhuman.com --organization acme --token-stdin
64
+ ```
65
+
66
+ ```sh
67
+ supercode teams machine start # local door only
68
+ supercode open --new bash --detach # a pane; prints its contextKey
69
+ supercode open <ctx> # attach; ctrl-] detaches
70
+ supercode teams input <ctx> "make test"
71
+ supercode teams title set <ctx> "rate-limit rescue" # also names the owned tmux window
72
+ supercode teams suspend <ctx> # remote input refused; observe keeps streaming
73
+ supercode teams resume <ctx> --input "carry on" # the input that hands the pane back
74
+ supercode teams log verify # the hash chain across every rotation
75
+
76
+ supercode teams machines access grant laptop --to user:usr_… --caps observe,control --on pane:<ctx> --ttl 1h
77
+ supercode open <ctx> --on laptop # through the selected context's server
78
+ supercode discover --fleet # every reachable machine of the context
79
+ supercode open --new claude --on laptop # a pane on an enrolled machine
80
+ ```
81
+
82
+ ### Products served behind the daemon
83
+
84
+ Another product serves its own kind of target on the machine (Volter Browsers:
85
+ `browser-profile:<name>`) through the daemon's `product` stream, opened with
86
+ `{ target: '<kind>:<name>', ...params }`. The product calls `serveProduct` from `products.mjs`,
87
+ which listens on loopback and writes a receipt, `products/<name>.json`, naming the kinds it
88
+ serves, its endpoint and a token. The daemon admits the caller first. Then it dials the newest
89
+ live receipt's endpoint and tells the product the principal, the capabilities held on that
90
+ target and the caller's params. Each side first proves it holds the receipt's token, the
91
+ product before it hears anything, and the token never crosses the wire. The daemon then
92
+ relays text messages both ways, each under the channel's frame limit. The product enforces what each
93
+ capability admits on its kind. A grant on `*` reaches a product's target only for `view`.
94
+
95
+ A remote caller is re-admitted every 15 s and when a grant it holds expires. The daemon decides
96
+ from the grants it holds, then refreshes them from the server, at most once per interval.
97
+ Wider capabilities are passed on; narrower ones close the stream. The server closes a
98
+ member's channels when their membership is deactivated, or when a group they reached a machine
99
+ through loses them or is deleted.
100
+
101
+ `supercode teams browser <profile> --machine <name>` serves a machine's browser profile as a
102
+ local CDP endpoint: `chromium.connectOverCDP('<printed address>')` drives it, each connection
103
+ riding its own `product` stream. The machine's own CDP address and Volter Browsers' publication token
104
+ never leave it. The endpoint answers any local process that reaches it, like a browser's own
105
+ debugging port.
106
+
107
+ A target whose kind has no live receipt is refused as not served on this machine. The
108
+ harness's own kinds (`pane`, `runtime`, `root`, `port`) are never served by a product.
109
+
110
+ ## Files (under `$SUPERCODE_HOME/teams`)
111
+
112
+ `machine.key|pub` (the host key; it signs `describe` and nothing else) · `machine.sock` ·
113
+ `machine.json` · `launches.jsonl` · `log.jsonl` (hash-chained, rotating to `log.<ts>.jsonl`) ·
114
+ `attention.json` · `titles.json` · `suspended.json` · `limits.json` · `products/` (receipts of
115
+ products served behind the daemon) · `workspaces/` (Teams contexts, credentials and connector
116
+ enrollments).
117
+
118
+ `limits.json` is read when the daemon starts: `maxAttachmentsPerPrincipal` (16),
119
+ `protocolFloor` (2), `logRotateBytes` (16 MiB), `logKeep` (8).
package/attention.mjs ADDED
@@ -0,0 +1,191 @@
1
+ // Attention. The split this file exists to hold:
2
+ //
3
+ // the terminal snapshot carries the EVIDENCE — what the Machine can see about
4
+ // a pane: is it live, is a foreground command running, does the screen show a
5
+ // crash or a choice, how long since the screen last moved, and whether anyone
6
+ // declared that this pane needs a human;
7
+ //
8
+ // the node owns `attention.request|clear` and a RANK TABLE as configuration;
9
+ //
10
+ // the label is a PURE FUNCTION of the two, computed on the Machine.
11
+ //
12
+ // Nothing here routes, acks, silences or notifies: `computeLabel`
13
+ // says what the evidence means and `notifies` says whether the label is the kind
14
+ // that warrants interrupting a human. WHO gets paged, and whether they already
15
+ // acknowledged it, is the consumer's — this module has no idea a human exists.
16
+
17
+ /** Normalized liveness of a pane: what the Machine could prove about whether
18
+ * anything is running in it.
19
+ * ended — no live pane with that context key
20
+ * running — a foreground command other than the pane's own shell
21
+ * waiting — the pane's shell is the foreground process: a prompt, turn over
22
+ * unknown — the pane exists but named no foreground command (the honest
23
+ * floor: absence of evidence, never silently reported as running)
24
+ */
25
+ export const LIVENESS = Object.freeze(['ended', 'running', 'unknown', 'waiting']);
26
+
27
+ /** Every label the vocabulary has, in the default precedence — the order
28
+ * `computeLabel` walks, first guard to match wins. The precedence is data,
29
+ * not control flow, and reads as one line:
30
+ *
31
+ * requested > ended > errored > asking > awaiting_me > stuck > unknown > working
32
+ *
33
+ * requested — someone (usually the agent in the pane) DECLARED it needs a
34
+ * human. Outranks even errored/asking: a declaration is stronger
35
+ * evidence than a screen read, and may be raised for a reason no
36
+ * screen heuristic could ever detect ("I finished, review my PR").
37
+ * It outranks `ended` too, so a request survives its pane.
38
+ * ended — the pane is gone. Checked before the screen rungs because there
39
+ * is no screen left to read; whatever the last capture said, it is
40
+ * not evidence about a pane that no longer exists.
41
+ * errored — the screen shows the agent's own call failing.
42
+ * asking — the screen shows a choice/permission prompt: blocked on an answer.
43
+ * awaiting_me — the shell is at its prompt: the turn ended, it waits on input.
44
+ * stuck — running, but the screen has not moved in `stuckIdleSeconds`.
45
+ * Looks busy, likely wedged. A heuristic, and labelled as one.
46
+ * unknown — live, but no rung said anything. A real look is warranted; it
47
+ * never outranks a state we affirmatively detected.
48
+ * working — live and running. The floor: always true, so the walk terminates.
49
+ */
50
+ export const LABELS = Object.freeze(['requested', 'ended', 'errored', 'asking', 'awaiting_me', 'stuck', 'unknown', 'working']);
51
+
52
+ /** The labels that warrant interrupting a human.
53
+ *
54
+ * `unknown` is deliberately NOT here: it is an UNPROVEN read — no rung
55
+ * matched this tick — not an affirmative signal. A pane's first capture is
56
+ * often empty, so notifying on `unknown` would page a human every time a pane
57
+ * is born and every time a capture is momentarily unreadable. It still sorts
58
+ * ahead of `working` in the order above, so it is visible to anyone already
59
+ * looking at the list, without paging anyone on a transient miss.
60
+ *
61
+ * `ended` and `working` are absent for the opposite reason: they are proven,
62
+ * and proven-to-be-fine needs nobody.
63
+ *
64
+ * `requested` is absent too, and that one is a deliberate default rather than
65
+ * a reasoned exclusion. It reads oddly — a label whose entire content is "a
66
+ * human is needed here" reports `notify: false` — and an org that wants
67
+ * declarations to page adds one word to its own `notify` list. Written down
68
+ * here so the next reader finds the question already asked.
69
+ */
70
+ export const DEFAULT_NOTIFY = Object.freeze(['errored', 'asking', 'awaiting_me', 'stuck']);
71
+
72
+ /** A foreground-running pane whose screen has not moved in this long looks busy
73
+ * but is likely wedged (a hung tool call, a crash without an exit). Heuristic,
74
+ * which is why it is configuration: 5 minutes suits an agent mid-turn and is
75
+ * far too long for a fleet of one-second jobs. */
76
+ export const DEFAULT_STUCK_IDLE_SECONDS = 300;
77
+
78
+ /** The rank table as it ships. It lives in `<home>/attention.json` under `rank`,
79
+ * beside the `requests` map, and the ORG EDITS THE FILE — the node re-reads it
80
+ * on every call, so a precedence change takes effect on the next `attention ls`
81
+ * with nothing restarted. Three knobs, and no more:
82
+ *
83
+ * order the precedence walk. Reorder it to change which evidence
84
+ * wins an argument; you cannot invent a label, because a
85
+ * label without a guard below is a label nothing can prove.
86
+ * stuckIdleSeconds how long a silent screen stays `working` before `stuck`.
87
+ * notify which labels warrant interrupting a human.
88
+ */
89
+ export const DEFAULT_RANK = Object.freeze({
90
+ order: LABELS,
91
+ stuckIdleSeconds: DEFAULT_STUCK_IDLE_SECONDS,
92
+ notify: DEFAULT_NOTIFY,
93
+ });
94
+
95
+ /** What it takes to prove each label, given the evidence tuple. One guard per
96
+ * label; the table above only decides the ORDER they are asked in. Guards are
97
+ * not configuration: a guard is the meaning of the word, and an org that could
98
+ * edit them would be renaming `errored` rather than re-ranking it. */
99
+ const GUARDS = Object.freeze({
100
+ requested: (e) => Boolean(e.requested),
101
+ ended: (e) => e.liveness === 'ended',
102
+ errored: (e) => e.signal === 'errored',
103
+ asking: (e) => e.signal === 'asking',
104
+ awaiting_me: (e) => e.liveness === 'waiting',
105
+ stuck: (e, table) => e.liveness === 'running' && (e.idleSeconds ?? 0) >= table.stuckIdleSeconds,
106
+ unknown: (e) => e.liveness === 'unknown',
107
+ working: () => true,
108
+ });
109
+
110
+ /** The rung each label's verdict actually came from, in the substrate's own
111
+ * `DerivationSource` vocabulary (sdk/terminal/substrate/session-lifecycle.d.ts)
112
+ * so the node speaks one evidence grammar rather than a second private one:
113
+ * agent-declared somebody told the node directly (`attention request`)
114
+ * process-tree tmux session enumeration and its foreground command
115
+ * screen-heuristic `classifyAttentionSignal` over the pane's capture
116
+ * transcript the capture-hash plateau that produces `stuck`
117
+ */
118
+ const SOURCES = Object.freeze({
119
+ requested: 'agent-declared',
120
+ ended: 'process-tree',
121
+ errored: 'screen-heuristic',
122
+ asking: 'screen-heuristic',
123
+ awaiting_me: 'process-tree',
124
+ stuck: 'transcript',
125
+ unknown: 'process-tree',
126
+ working: 'process-tree',
127
+ });
128
+
129
+ /** Merge an org's `rank` into the defaults so the walk is always total: unknown
130
+ * names are dropped (a label nothing can prove would silently swallow the rest
131
+ * of the order), and any label the org left out is appended in default order
132
+ * rather than made unreachable — omitting `ended` must not mean a dead pane has
133
+ * no label. */
134
+ export function normalizeRank(raw) {
135
+ const table = raw && typeof raw === 'object' ? raw : {};
136
+ const wanted = Array.isArray(table.order) ? table.order.filter((label) => LABELS.includes(label)) : [];
137
+ const order = [...new Set([...wanted, ...LABELS])];
138
+ const notify = Array.isArray(table.notify) ? table.notify.filter((label) => LABELS.includes(label)) : DEFAULT_NOTIFY;
139
+ const stuck = Number(table.stuckIdleSeconds);
140
+ return {
141
+ order,
142
+ stuckIdleSeconds: Number.isFinite(stuck) && stuck >= 0 ? stuck : DEFAULT_STUCK_IDLE_SECONDS,
143
+ notify: [...notify],
144
+ };
145
+ }
146
+
147
+ /**
148
+ * The label, from the four inputs and the table. Pure: same tuple, same table,
149
+ * same answer, on any Machine, with no clock and no I/O of its own.
150
+ *
151
+ * @param {{requested?: unknown, signal?: 'asking'|'errored'|null, liveness: 'ended'|'running'|'unknown'|'waiting', idleSeconds?: number|null}} evidence
152
+ * @param {{order: string[], stuckIdleSeconds: number, notify: string[]}} [rankTable]
153
+ * @returns {string} one of LABELS
154
+ */
155
+ export function computeLabel(evidence, rankTable = DEFAULT_RANK) {
156
+ const table = normalizeRank(rankTable);
157
+ for (const label of table.order) {
158
+ if (GUARDS[label](evidence, table)) return label;
159
+ }
160
+ return 'working';
161
+ }
162
+
163
+ /** Where in the precedence a label sits — the index the caller sorts by, so a
164
+ * list ordered urgent-first never needs its own copy of the order. */
165
+ export function rankOf(label, rankTable = DEFAULT_RANK) {
166
+ return normalizeRank(rankTable).order.indexOf(label);
167
+ }
168
+
169
+ /** Whether this label is the kind that warrants interrupting a human. The node
170
+ * reports the flag; who is interrupted, and whether they already said "I know",
171
+ * belongs to whoever is reading. */
172
+ export function notifies(label, rankTable = DEFAULT_RANK) {
173
+ return normalizeRank(rankTable).notify.includes(label);
174
+ }
175
+
176
+ /** The rung that decided this label, for the `source` the evidence carries. */
177
+ export function sourceOf(label) {
178
+ return SOURCES[label] ?? 'process-tree';
179
+ }
180
+
181
+ /** Labels decided by liveness alone: when the harness reported that liveness
182
+ * itself, the verdict came from its own lifecycle, not the process tree. */
183
+ const LIVENESS_LABELS = new Set(['awaiting_me', 'working', 'unknown', 'ended']);
184
+
185
+ /** Label + rank + notify in one call, for the node's row builder. */
186
+ export function judge(evidence, rankTable = DEFAULT_RANK) {
187
+ const table = normalizeRank(rankTable);
188
+ const attention = computeLabel(evidence, table);
189
+ const source = evidence.livenessSource && LIVENESS_LABELS.has(attention) ? evidence.livenessSource : sourceOf(attention);
190
+ return { attention, rank: table.order.indexOf(attention), notify: table.notify.includes(attention), source };
191
+ }
package/bin/fleet.mjs ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ // The fleet CLI. `supercode discover --fleet`, `supercode discover --on M`,
3
+ // `supercode open [--new] [--on M]` and `supercode harness drive --on M` spawn this; it is also
4
+ // runnable directly as `supercode-teams-fleet …`. It mints no verb of its own:
5
+ // every verb here is the remote arm of a verb supercode already has.
6
+ //
7
+ // discover [--query q] [--limit n] [--machines a,b] [--context c]
8
+ // harness.v1.sessions.discover on every machine of the selected Teams
9
+ // context the caller can reach (or the named subset), merged into one
10
+ // page; each unreachable machine is a row of its own. Nothing is stored.
11
+ // rpc --on <machine> <harness.v1 method> [json] one operator request, one machine
12
+ // launch [--on <machine>] <program> [--cwd d] [--launch-key k] [--kind k] [--input text] -- args
13
+ // (`open --new`)
14
+ // attach [--on <machine>] <pane> [--mode observe|control] ctrl-] detaches (`open --on`)
15
+ //
16
+ // `--on` names an enrolled machine of the selected context, reached through
17
+ // its Teams server. Omitted, every verb acts on this OS user's own daemon, so
18
+ // the remote arm and the local arm are one code path.
19
+ import { attachPane, attachRuntime } from '../client.mjs';
20
+ import { openOn, harnessRequest, nodeRequest, fleetDiscover } from '../fleet.mjs';
21
+
22
+ const argv = process.argv.slice(2);
23
+ const flags = {};
24
+ const positional = [];
25
+ for (let i = 0; i < argv.length; i += 1) {
26
+ const a = argv[i];
27
+ if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
28
+ if (a.startsWith('--')) {
29
+ const eq = a.indexOf('=');
30
+ if (eq > 0) { flags[a.slice(2, eq)] = a.slice(eq + 1); continue; } // --args=--norc
31
+ const key = a.slice(2);
32
+ const next = argv[i + 1];
33
+ if (next !== undefined && !next.startsWith('--')) { flags[key] = next; i += 1; } else flags[key] = true;
34
+ } else positional.push(a);
35
+ }
36
+ const [verb, sub, ...rest] = positional;
37
+
38
+ const out = (value) => process.stdout.write((typeof value === 'string' ? value : JSON.stringify(value, null, 2)) + '\n');
39
+ const die = (message, code = 2) => { process.stderr.write(`supercode fleet: ${message}\n`); process.exit(code); };
40
+ const onName = () => (typeof flags.on === 'string' ? flags.on : null);
41
+ const contextName = () => (typeof flags.context === 'string' ? flags.context : null);
42
+
43
+ /** The machine `--on` names in the selected context, or this box's own daemon. */
44
+ function channelForOn() {
45
+ return openOn(onName(), { context: contextName() });
46
+ }
47
+
48
+ async function main() {
49
+ switch (verb) {
50
+ case 'discover': {
51
+ const params = {};
52
+ if (typeof flags.query === 'string') params.query = flags.query;
53
+ if (flags.limit !== undefined) params.limit = Number(flags.limit);
54
+ if (typeof flags.workspace === 'string') params.workspace = flags.workspace;
55
+ if (flags.harness) params.harnesses = String(flags.harness).split(',').filter(Boolean);
56
+ const machines = typeof flags.machines === 'string' ? String(flags.machines).split(',').filter(Boolean) : null;
57
+ out(await fleetDiscover({ context: contextName(), machines, params }));
58
+ return;
59
+ }
60
+ case 'rpc': {
61
+ if (!sub) die('rpc --on <machine> <harness.v1 method> [json]');
62
+ const channel = await channelForOn();
63
+ try {
64
+ out(await harnessRequest(channel, sub, rest[0] ? JSON.parse(rest[0]) : {}));
65
+ } finally {
66
+ channel.close();
67
+ }
68
+ return;
69
+ }
70
+ case 'launch': {
71
+ // The node's own `launch` door: a pane on the Machine, keyed so a
72
+ // retry of the same launch key never opens a second one.
73
+ const program = typeof flags.program === 'string' ? flags.program : sub;
74
+ if (!program) die('launch --on <machine> --program <program> [--args=…]');
75
+ const channel = await channelForOn();
76
+ try {
77
+ out(await nodeRequest(channel, 'launch', {
78
+ key: flags['launch-key'],
79
+ pane: flags.pane,
80
+ kind: flags.kind,
81
+ program,
82
+ arguments: flags.args ? String(flags.args).split(' ').filter(Boolean) : rest,
83
+ cwd: flags.cwd,
84
+ initialInput: flags.input,
85
+ name: flags.name,
86
+ }));
87
+ } finally {
88
+ channel.close();
89
+ }
90
+ return;
91
+ }
92
+ case 'attach': {
93
+ const pane = sub;
94
+ if (!pane && !flags.runtime) die('attach --on <machine> <pane> [--mode observe|control] | attach --on <machine> --runtime <runtime-id> [--send text]');
95
+ const mode = typeof flags.mode === 'string' ? flags.mode : 'observe';
96
+ const channel = await channelForOn();
97
+ if (flags.runtime || (pane && pane.includes('://'))) {
98
+ // A MANAGED runtime: frontend.v2 over the channel . The node mints this principal
99
+ // its own credential, so control and approvals are the lease coordinator's own semantics.
100
+ const runtime = String(flags.runtime ?? pane).replace(/^supercode-live:\/\//, '');
101
+ const fc = await attachRuntime(channel, { runtime, permissions: mode === 'control' ? ['observe', 'interact', 'approve'] : ['observe'] });
102
+ const descriptor = await fc.describe();
103
+ process.stderr.write(`[attached runtime ${runtime} (${descriptor.harness ?? descriptor.title ?? 'runtime'}) — ${mode}${mode === 'control' ? '; ctrl-] detaches' : ''}]\n`);
104
+ const snapshot = await fc.attach(Number(flags.tail ?? 20));
105
+ for (const cell of snapshot.cells ?? snapshot.messages ?? []) {
106
+ const text = cell.text ?? cell.content ?? '';
107
+ if (text) process.stdout.write(`${cell.role ?? cell.kind ?? ''}: ${String(text).slice(0, 400)}\n`);
108
+ }
109
+ fc.on('event', (event) => {
110
+ const text = event.text ?? event.payload?.text ?? null;
111
+ if (event.type === 'output_delta' && text) process.stdout.write(text);
112
+ else if (event.type && event.type !== 'output_delta') process.stderr.write(`[${event.type}${text ? ': ' + String(text).slice(0, 120) : ''}]\n`);
113
+ });
114
+ fc.on('disconnect', () => { process.stderr.write('\n[runtime detached]\n'); channel.close(); process.exit(0); });
115
+ if (mode === 'control') await fc.takeControl();
116
+ if (typeof flags.send === 'string') { await fc.sendInput(flags.send); process.stderr.write(`[sent]\n`); }
117
+ if (mode === 'control' && process.stdin.isTTY) {
118
+ process.stdin.setRawMode(false);
119
+ let line = '';
120
+ process.stdin.on('data', async (chunk) => {
121
+ const s = chunk.toString('utf8');
122
+ if (s.includes('')) { fc.detachChannel(); channel.close(); process.exit(0); }
123
+ line += s;
124
+ if (line.endsWith('\n')) { const prompt = line.trimEnd(); line = ''; if (prompt) await fc.sendInput(prompt).catch((e) => process.stderr.write(`[${e.message}]\n`)); }
125
+ });
126
+ }
127
+ await fc.startEvents({}).catch((e) => { process.stderr.write(`[events ended: ${e.message}]\n`); });
128
+ if (typeof flags.for === 'string') setTimeout(() => { fc.detachChannel(); channel.close(); process.exit(0); }, Number(flags.for));
129
+ return;
130
+ }
131
+ const term = await attachPane(channel, { pane, mode, cols: process.stdout.columns ?? 120, rows: process.stdout.rows ?? 40 });
132
+ term.on('hello', (frame) => process.stderr.write(`[attached ${pane} ${frame.cols}x${frame.rows}${mode === 'control' ? ' — ctrl-] detaches' : ''}]\n`));
133
+ term.on('error-message', (message) => process.stderr.write(`[${message}]\n`));
134
+ term.on('data', (text) => process.stdout.write(text));
135
+ term.on('close', (reason) => { process.stderr.write(`\n[detached${reason ? `: ${reason}` : ''}]\n`); channel.close(); process.exit(0); });
136
+ if (mode === 'control' && process.stdin.isTTY) {
137
+ process.stdin.setRawMode(true);
138
+ process.stdin.on('data', (chunk) => {
139
+ if (chunk[0] === 0x1d) { term.close(); return; } // ctrl-] detaches
140
+ term.write(chunk.toString('utf8'));
141
+ });
142
+ }
143
+ process.stdout.on('resize', () => term.resize(process.stdout.columns, process.stdout.rows));
144
+ return;
145
+ }
146
+ default:
147
+ die('verbs: discover, rpc, launch, attach');
148
+ }
149
+ }
150
+
151
+ main().catch((error) => die(error.message ?? String(error), 1));