@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.
- package/README.md +119 -0
- package/attention.mjs +191 -0
- package/bin/fleet.mjs +151 -0
- package/bin/teams.mjs +291 -0
- package/bin/workspace/args.mjs +66 -0
- package/bin/workspace/files.mjs +110 -0
- package/bin/workspace/lifecycle.mjs +113 -0
- package/bin/workspace/volter-device.mjs +42 -0
- package/bin/workspace/volter-login.mjs +185 -0
- package/bin/workspace.mjs +726 -0
- package/browser-attach.mjs +90 -0
- package/client.mjs +212 -0
- package/connector/effects.mjs +95 -0
- package/connector/index.d.ts +69 -0
- package/connector/index.mjs +174 -0
- package/connector/native-continuation.d.ts +8 -0
- package/connector/native-continuation.mjs +50 -0
- package/connector/native-effects.mjs +164 -0
- package/context.mjs +34 -0
- package/contracts/adapter-map.json +16 -0
- package/contracts/index.d.ts +299 -0
- package/contracts/index.mjs +37 -0
- package/execution.mjs +206 -0
- package/fleet.mjs +78 -0
- package/index.mjs +12 -0
- package/machine-door.mjs +36 -0
- package/machine.mjs +1398 -0
- package/package.json +80 -0
- package/porthole-hook.cjs +137 -0
- package/remote/job-exec.py +146 -0
- package/remote/wire.py +23 -0
- package/server/arc2-transactions.mjs +125 -0
- package/server/attachment-relay.mjs +149 -0
- package/server/catalog.mjs +7 -0
- package/server/continuations.mjs +56 -0
- package/server/correlation.d.ts +4 -0
- package/server/correlation.mjs +17 -0
- package/server/effects-transactions.mjs +119 -0
- package/server/effects.mjs +97 -0
- package/server/event-routes.mjs +208 -0
- package/server/exports.mjs +46 -0
- package/server/harness-routes.mjs +248 -0
- package/server/index.d.ts +31 -0
- package/server/index.mjs +52 -0
- package/server/ingest.mjs +24 -0
- package/server/node-upgrade.mjs +39 -0
- package/server/otlp/collector-config.d.ts +2 -0
- package/server/otlp/collector-config.mjs +26 -0
- package/server/otlp/decoders/index.d.ts +63 -0
- package/server/otlp/decoders/index.mjs +311 -0
- package/server/otlp/index.d.ts +4 -0
- package/server/otlp/index.mjs +4 -0
- package/server/otlp/projection.d.ts +5 -0
- package/server/otlp/projection.mjs +17 -0
- package/server/otlp/receiver.d.ts +3 -0
- package/server/otlp/receiver.mjs +36 -0
- package/server/otlp/schema/google/protobuf/any.proto +162 -0
- package/server/otlp/schema/google/rpc/status.proto +48 -0
- package/server/otlp/schema/manifest.json +22 -0
- package/server/otlp/schema/opentelemetry/proto/collector/logs/v1/logs_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/collector/metrics/v1/metrics_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/collector/trace/v1/trace_service.proto +77 -0
- package/server/otlp/schema/opentelemetry/proto/common/v1/common.proto +154 -0
- package/server/otlp/schema/opentelemetry/proto/logs/v1/logs.proto +226 -0
- package/server/otlp/schema/opentelemetry/proto/metrics/v1/metrics.proto +735 -0
- package/server/otlp/schema/opentelemetry/proto/resource/v1/resource.proto +45 -0
- package/server/otlp/schema/opentelemetry/proto/trace/v1/trace.proto +359 -0
- package/server/otlp/transport.d.ts +10 -0
- package/server/otlp/transport.mjs +424 -0
- package/server/otlp/wire-codec.mjs +153 -0
- package/server/store-core.mjs +139 -0
- package/server/store-worker.mjs +23 -0
- package/worker/fetch-adapter.mjs +49 -0
- package/worker/index.mjs +127 -0
- package/worker/websocket-duplex.mjs +29 -0
- package/worker/wrangler.toml +38 -0
- package/workspace-client.d.ts +75 -0
- package/workspace-client.mjs +171 -0
- package/workspace-move.mjs +38 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/** Design-time contract. Copy/generate public declarations in B01; do not import this from production. */
|
|
2
|
+
import type { NormalizedMessage, Fidelity, HarnessId, ObservedRuntimeEvent } from '@volter/supercode-harness-sdk';
|
|
3
|
+
export type { NormalizedMessage, Fidelity, HarnessId, ObservedRuntimeEvent } from '@volter/supercode-harness-sdk';
|
|
4
|
+
|
|
5
|
+
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
|
|
6
|
+
export type Id = string; // kind prefix + '_' + lowercase UUID without hyphens; see protocol.md
|
|
7
|
+
export type Millis = number; // integer, 0..Number.MAX_SAFE_INTEGER
|
|
8
|
+
export type Revision = number; // integer, 0..Number.MAX_SAFE_INTEGER
|
|
9
|
+
export type TeamRole = 'owner' | 'admin' | 'member' | 'viewer';
|
|
10
|
+
export type SessionRole = 'reader' | 'manager';
|
|
11
|
+
export type Visibility = 'team' | 'restricted';
|
|
12
|
+
export type CredentialKind = 'user' | 'service' | 'machine' | 'ingest';
|
|
13
|
+
export type SourceKind = 'connector' | 'otlp';
|
|
14
|
+
export type SourceState = 'active' | 'paused' | 'revoked';
|
|
15
|
+
export type CaptureMode = 'none' | 'since' | 'all';
|
|
16
|
+
export interface Scope { server_id: Id; team_id: Id; principal_id: Id }
|
|
17
|
+
export interface ServerScope { server_id: Id; principal_id: Id }
|
|
18
|
+
export interface ServerPage<T> { version: 1; context: ServerScope; items: T[]; next_cursor: string | null }
|
|
19
|
+
export interface Page<T> { version: 1; context: Scope; items: T[]; next_cursor: string | null }
|
|
20
|
+
export interface Result<T> { version: 1; context: Scope; data: T }
|
|
21
|
+
export type ErrorCode =
|
|
22
|
+
| 'invalid_request' | 'ambiguous_reference' | 'authentication_required' | 'not_found'
|
|
23
|
+
| 'forbidden' | 'unsupported' | 'machine_offline' | 'controller_busy' | 'conflict'
|
|
24
|
+
| 'resync_required' | 'revision_mismatch' | 'idempotency_conflict' | 'source_revoked'
|
|
25
|
+
| 'source_paused' | 'session_deleted' | 'batch_gap' | 'capture_conflict'
|
|
26
|
+
| 'fidelity_unavailable' | 'rate_limited' | 'request_too_large' | 'internal_error';
|
|
27
|
+
export interface Failure {
|
|
28
|
+
version: 1;
|
|
29
|
+
error: { code: ErrorCode; message: string; request_id: Id; retryable: boolean; details: Json };
|
|
30
|
+
}
|
|
31
|
+
export interface Principal { id: Id; kind: 'user' | 'service'; display_name: string }
|
|
32
|
+
export interface Membership { team_id: Id; principal: Principal; role: TeamRole; revision: Revision }
|
|
33
|
+
export interface Group { id: Id; name: string; revision: Revision; member_ids: Id[] }
|
|
34
|
+
export interface Team { id: Id; name: string; revision: Revision }
|
|
35
|
+
export interface Me { server_id: Id; principal: Principal; teams: { team: Team; role: TeamRole }[] }
|
|
36
|
+
export interface GrantSubject { kind: 'principal' | 'group'; id: Id }
|
|
37
|
+
export interface SessionGrant { id: Id; subject: GrantSubject; role: SessionRole; expires_at: Millis | null }
|
|
38
|
+
export interface Access {
|
|
39
|
+
revision: Revision; visibility: Visibility; custodian_id: Id; grants: SessionGrant[];
|
|
40
|
+
}
|
|
41
|
+
export interface Attribution {
|
|
42
|
+
principal_id: Id | null;
|
|
43
|
+
reported_user: string | null;
|
|
44
|
+
status: 'verified' | 'reported' | 'unknown';
|
|
45
|
+
mapping_id: Id | null;
|
|
46
|
+
}
|
|
47
|
+
export interface Repository { authority: string; path: string; key: string }
|
|
48
|
+
export interface Source {
|
|
49
|
+
id: Id; name: string; kind: SourceKind; state: SourceState; machine_id: Id | null;
|
|
50
|
+
custodian_id: Id; default_visibility: Visibility; revision: Revision;
|
|
51
|
+
last_received_at: Millis | null; decoder: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface Machine {
|
|
54
|
+
id: Id; name: string; custodian_id: Id; enrollment: 'active' | 'revoked';
|
|
55
|
+
connection: 'online' | 'offline'; last_seen_at: Millis | null; incarnation_id: Id | null;
|
|
56
|
+
/** The key id the machine signs `describe` with; null until its daemon reports one. */
|
|
57
|
+
host_key_id: string | null;
|
|
58
|
+
revision: Revision;
|
|
59
|
+
}
|
|
60
|
+
export interface Root { id: Id; machine_id: Id; name: string; harnesses: HarnessId[] }
|
|
61
|
+
export interface Coverage {
|
|
62
|
+
completeness: 'unknown' | 'partial' | 'complete';
|
|
63
|
+
reasons: ('prefix_not_captured' | 'sync_paused' | 'redacted' | 'truncated' | 'sampled'
|
|
64
|
+
| 'unsequenced' | 'source_conflict' | 'unsupported_decoder' | 'retention' | 'parse_error')[];
|
|
65
|
+
missing: { start_at: Millis | null; end_at: Millis | null; reason: string }[];
|
|
66
|
+
}
|
|
67
|
+
export type Action = 'read' | 'follow' | 'export_text' | 'export_native' | 'manage'
|
|
68
|
+
| 'attach_observe' | 'interact' | 'approve' | 'interrupt' | 'terminate' | 'continue';
|
|
69
|
+
export interface Eligibility {
|
|
70
|
+
allowed: boolean;
|
|
71
|
+
reason: 'forbidden' | 'machine_offline' | 'unsupported' | 'missing_context' | 'controller_busy' | null;
|
|
72
|
+
}
|
|
73
|
+
export interface Binding {
|
|
74
|
+
id: Id; machine_id: Id; incarnation_id: Id; native_runtime_id: string; native_session_id: string;
|
|
75
|
+
harness: HarnessId; state: 'running' | 'stopped' | 'unknown'; observed_at: Millis;
|
|
76
|
+
native_capabilities: Action[];
|
|
77
|
+
}
|
|
78
|
+
export interface Descriptor {
|
|
79
|
+
id: Id; revision: Revision; title: string | null; harness: HarnessId; user: Attribution;
|
|
80
|
+
repository: Repository | null; parent_id: Id | null; continued_from: Id | null;
|
|
81
|
+
recorded_activity_at: Millis | null; received_at: Millis; sort_at: Millis;
|
|
82
|
+
activity_basis: 'recorded' | 'received'; source_ids: Id[]; machine_ids: Id[];
|
|
83
|
+
coverage: Coverage; bindings: Binding[]; actions: Record<Action, Eligibility>;
|
|
84
|
+
}
|
|
85
|
+
export interface Message {
|
|
86
|
+
id: Id; source_id: Id; native_key: string; recorded_at: Millis | null;
|
|
87
|
+
value: NormalizedMessage; // reuse core fields; do not create another chat model
|
|
88
|
+
}
|
|
89
|
+
export interface ReadResult {
|
|
90
|
+
version: 1; context: Scope; descriptor: Descriptor; messages: Message[];
|
|
91
|
+
before_cursor: string | null; event_cursor: string; revision: Revision;
|
|
92
|
+
display: { returned: number; total: number; truncated_fields: number };
|
|
93
|
+
}
|
|
94
|
+
export interface ListQuery {
|
|
95
|
+
user?: Id | 'me'; repo?: string; harness?: HarnessId; machine?: Id; source?: Id;
|
|
96
|
+
since?: Millis; until?: Millis; limit?: number; cursor?: string; include_children?: boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface SearchQuery extends ListQuery { query: string }
|
|
99
|
+
export interface SearchHit {
|
|
100
|
+
session: Descriptor;
|
|
101
|
+
matches: { message_id: Id | null; field: 'title' | 'content'; excerpt: string; offset: number }[];
|
|
102
|
+
match_at: Millis; indexed_revision: Revision;
|
|
103
|
+
}
|
|
104
|
+
export interface SearchPage extends Page<SearchHit> {
|
|
105
|
+
coverage: { scope: 'retained_text'; indexed_at: Millis; indexed_revision: Revision };
|
|
106
|
+
}
|
|
107
|
+
export interface ReadQuery { tail?: number; before?: string; revision?: Revision }
|
|
108
|
+
export interface FollowQuery { after?: string; tail?: number }
|
|
109
|
+
export type Event =
|
|
110
|
+
| { type: 'snapshot'; cursor: string; revision: Revision; data: ReadResult }
|
|
111
|
+
| { type: 'session_changed'; cursor: string; revision: Revision; session_id: Id; reason: 'capture' | 'metadata' | 'coverage' | 'binding' | 'merge'; read_required: true }
|
|
112
|
+
| { type: 'session_deleted'; cursor: string; revision: Revision; session_id: Id }
|
|
113
|
+
| { type: 'catalog_reset'; cursor: string; revision: Revision; data: Page<Descriptor> }
|
|
114
|
+
| { type: 'access_revoked'; reason: 'membership' | 'session' | 'credential'; terminal: true }
|
|
115
|
+
| { type: 'resync_required'; terminal: true };
|
|
116
|
+
|
|
117
|
+
export interface ContextFile {
|
|
118
|
+
version: 1; current: string | null;
|
|
119
|
+
contexts: Record<string, { origin: string; server_id: Id; team_id: Id; principal_id: Id; credential_ref: string }>;
|
|
120
|
+
}
|
|
121
|
+
export interface TokenCredential { token: string; kind: CredentialKind; expires_at: Millis | null }
|
|
122
|
+
export interface CreateInvitation { display_name: string; role: Exclude<TeamRole, 'owner'>; expires_in_ms: number }
|
|
123
|
+
export interface Invitation { id: Id; role: Exclude<TeamRole, 'owner'>; expires_at: Millis; enrollment_code?: string }
|
|
124
|
+
export interface AcceptInvitation { enrollment_code: string }
|
|
125
|
+
export interface CreateSource { name: string; kind: 'otlp'; custodian_id: Id; visibility: Visibility }
|
|
126
|
+
export interface SourceCollectorConfig {
|
|
127
|
+
format: 'otel-collector'; source_id: Id; endpoint: string; credential_env: string; content: string;
|
|
128
|
+
}
|
|
129
|
+
export interface EnrollMachine { name: string; enrollment_key: string }
|
|
130
|
+
export interface Enrollment { machine: Machine; source: Source; credential: TokenCredential | null }
|
|
131
|
+
export interface Heartbeat { incarnation_id: Id; sequence: number; bindings: Binding[]; roots: Root[]; host_key_id?: string }
|
|
132
|
+
export interface SyncRule {
|
|
133
|
+
id: Id; machine_id: Id; source_id: Id; selector: { root_id: Id; harnesses: HarnessId[] };
|
|
134
|
+
visibility: Visibility; backfill: { mode: CaptureMode; since: Millis | null };
|
|
135
|
+
state: 'active' | 'paused' | 'removed'; revision: Revision; capture_after: Millis;
|
|
136
|
+
}
|
|
137
|
+
export interface ConnectorConfiguration {
|
|
138
|
+
machine: Machine; source: Source; rules: SyncRule[]; authorization_revision: Revision;
|
|
139
|
+
/** Who holds the machine implicitly as operator, and the machine grants its daemon admits from. */
|
|
140
|
+
custodian_id: Id; machine_grants: MachineGrant[];
|
|
141
|
+
}
|
|
142
|
+
/** The machine daemon's door vocabulary; `operate` is the whole machine (harness.v1 and launch) and targets only `*`. */
|
|
143
|
+
export type MachineCapability = 'operate' | 'view' | 'observe' | 'interact' | 'control' | 'files' | 'ports' | 'mail';
|
|
144
|
+
/** A subject's capabilities on one machine's doors (design §11): targets are `*` (operate and view only) or `<kind>:<name>` — the harness serves `pane:`, `runtime:`, `root:/path` and `port:<pane>/<n>` — or `<kind>:*` for every target of a kind. */
|
|
145
|
+
export interface MachineGrant {
|
|
146
|
+
id: Id; machine_id: Id; subject_id: Id; caps: MachineCapability[]; targets: string[];
|
|
147
|
+
issuer_id: Id; created_at: Millis; expires_at: Millis; revision: Revision;
|
|
148
|
+
}
|
|
149
|
+
export interface ArtifactChunk {
|
|
150
|
+
upload_id: Id; index: number; sha256: string; bytes_base64: string;
|
|
151
|
+
}
|
|
152
|
+
export interface ArtifactManifest {
|
|
153
|
+
upload_id: Id; sha256: string; byte_length: number; chunks: number;
|
|
154
|
+
media_type: string; role: 'native' | 'sidecar' | 'otlp'; relative_name: string;
|
|
155
|
+
}
|
|
156
|
+
export interface CaptureBegin {
|
|
157
|
+
native_session_id: string; harness: HarnessId; generation: number; sequence: number;
|
|
158
|
+
capture_id: Id; previous_digest: string | null; visibility: Visibility;
|
|
159
|
+
rule_id: Id | null; rule_revision: Revision | null;
|
|
160
|
+
}
|
|
161
|
+
export type CaptureBeginResult =
|
|
162
|
+
| { state: 'staging'; capture_id: Id; next_part: number }
|
|
163
|
+
| { state: 'committed'; receipt: CaptureReceipt };
|
|
164
|
+
export interface CapturePart { capture_id: Id; index: number; messages: CapturedMessage[] }
|
|
165
|
+
export interface CapturedMessage {
|
|
166
|
+
native_key: string; recorded_at: Millis | null; value: NormalizedMessage;
|
|
167
|
+
}
|
|
168
|
+
export interface CapturedNativeBundle {
|
|
169
|
+
id: Id; harness: HarnessId; fidelity: Fidelity; residue: string[];
|
|
170
|
+
files: Array<{ upload_id: Id; role: ExportFile['role'] }>;
|
|
171
|
+
}
|
|
172
|
+
export interface CaptureCommit {
|
|
173
|
+
capture_id: Id; parts: number; digest: string; title: string | null;
|
|
174
|
+
repository: Repository | null; reported_user: string | null;
|
|
175
|
+
recorded_activity_at: Millis | null; parent_native_id: string | null;
|
|
176
|
+
coverage: Coverage; artifacts: ArtifactManifest[]; native_bundle?: CapturedNativeBundle | null;
|
|
177
|
+
}
|
|
178
|
+
export interface CaptureReceipt {
|
|
179
|
+
source_id: Id; capture_id: Id; session_id: Id; generation: number; sequence: number;
|
|
180
|
+
digest: string; revision: Revision; duplicate: boolean;
|
|
181
|
+
}
|
|
182
|
+
export interface Correlation {
|
|
183
|
+
id: Id; left_source_id: Id; left_native_id: string; right_source_id: Id; right_native_id: string;
|
|
184
|
+
left_revision: Revision; right_revision: Revision; harness: HarnessId;
|
|
185
|
+
evidence: 'custodian_attestation'; revision: Revision;
|
|
186
|
+
}
|
|
187
|
+
export interface LiveGrant {
|
|
188
|
+
id: Id; machine_id: Id; session_id: Id; binding_id: Id; subject_id: Id;
|
|
189
|
+
actions: Action[]; expires_at: Millis; revision: Revision;
|
|
190
|
+
}
|
|
191
|
+
export interface AttachmentRequest { binding_id: Id; mode: 'observe' | 'control' }
|
|
192
|
+
export type AttachmentFrame = ObservedRuntimeEvent;
|
|
193
|
+
export interface Attachment { id: Id; state: 'pending' | 'active' | 'detached' | 'revoked' | 'closed'; stream_format: 'observed_runtime_event'; binding_id: Id; expires_at: Millis; stream_url: string; credential: string; effective_actions: Action[] }
|
|
194
|
+
export interface PrepareContinuation {
|
|
195
|
+
source_revision: Revision; destination: { machine_id: Id | 'local'; root_id: Id | null; cwd: string };
|
|
196
|
+
harness: HarnessId; fidelity: Fidelity;
|
|
197
|
+
}
|
|
198
|
+
export interface Continuation {
|
|
199
|
+
id: Id; source_id: Id; source_revision: Revision; access_revision: Revision;
|
|
200
|
+
state: 'preparing' | 'ready' | 'refused' | 'expired' | 'launching' | 'running' | 'unknown' | 'failed';
|
|
201
|
+
requester_id: Id; destination: PrepareContinuation['destination']; harness: HarnessId;
|
|
202
|
+
fidelity: Fidelity;
|
|
203
|
+
expires_at: Millis; residue: string[]; operation_id: Id | null; new_session_id: Id | null;
|
|
204
|
+
destination_authorization: { principal_id: Id; approved_by: Id; expires_at: Millis } | null;
|
|
205
|
+
}
|
|
206
|
+
export interface Operation {
|
|
207
|
+
id: Id; state: 'pending' | 'running' | 'succeeded' | 'failed' | 'unknown';
|
|
208
|
+
result: Json; error: Failure['error'] | null;
|
|
209
|
+
}
|
|
210
|
+
/** Export bytes are fetched through the session ACL; no bearer or machine path appears here. */
|
|
211
|
+
export interface ExportFile {
|
|
212
|
+
upload_id: Id; relative_name: string; media_type: string; byte_length: number; sha256: string;
|
|
213
|
+
role: 'primary' | 'subagent' | 'bundle' | 'source_recovery';
|
|
214
|
+
}
|
|
215
|
+
export interface ExportResult {
|
|
216
|
+
kind: 'export'; source_session_id: Id; source_revision: Revision;
|
|
217
|
+
format: ExportRequest['format']; fidelity: Fidelity; residue: string[]; coverage: Coverage;
|
|
218
|
+
files: ExportFile[];
|
|
219
|
+
}
|
|
220
|
+
export interface ExportRequest { format: 'text' | 'html' | HarnessId; fidelity?: Fidelity; revision: Revision }
|
|
221
|
+
export interface EffectScope {
|
|
222
|
+
id: Id;
|
|
223
|
+
principal_id: Id; team_id: Id; machine_id: Id; binding_id: Id | null;
|
|
224
|
+
incarnation_id: Id; session_id: Id; access_revision: Revision; grant_id: Id | null;
|
|
225
|
+
expires_at: Millis;
|
|
226
|
+
}
|
|
227
|
+
export type NativeEffect = EffectScope & (
|
|
228
|
+
| { kind: 'attach'; payload: { attachment_id: Id; mode: 'observe' | 'control' } }
|
|
229
|
+
| { kind: 'input'; payload: { attachment_id: Id; text: string } }
|
|
230
|
+
| { kind: 'approve'; payload: { attachment_id: Id; request_id: string; decision: 'allow_once' | 'deny' } }
|
|
231
|
+
| { kind: 'interrupt' | 'terminate'; payload: { attachment_id: Id } }
|
|
232
|
+
| { kind: 'continue'; payload: { continuation_id: Id } }
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
export interface EffectDelivery { effect: NativeEffect; operation_id: Id; state: Operation['state'] }
|
|
236
|
+
export interface EffectClaim { delivery: EffectDelivery; execute: boolean }
|
|
237
|
+
export type EffectResult =
|
|
238
|
+
| { state: 'succeeded'; receipt: { native_session_id: string | null; runtime_id: string | null } }
|
|
239
|
+
| { state: 'failed'; error: Failure['error'] }
|
|
240
|
+
| { state: 'unknown'; error: null };
|
|
241
|
+
export interface LocalLaunchTicket {
|
|
242
|
+
kind: 'local_launch'; continuation_id: Id; operation_id: Id; harness: HarnessId; fidelity: Fidelity;
|
|
243
|
+
artifact: ExportResult;
|
|
244
|
+
destination: { machine_id: 'local'; root_id: null; cwd: string }; expires_at: Millis;
|
|
245
|
+
}
|
|
246
|
+
export interface RemoteLaunchTicket extends Omit<LocalLaunchTicket, 'kind' | 'destination'> {
|
|
247
|
+
kind: 'remote_launch'; destination: { machine_id: Id; root_id: Id; cwd: string };
|
|
248
|
+
}
|
|
249
|
+
export interface LocalCompletion {
|
|
250
|
+
operation_id: Id; outcome: 'running' | 'failed' | 'unknown';
|
|
251
|
+
native_session_id: string | null; error: Failure['error'] | null;
|
|
252
|
+
}
|
|
253
|
+
export interface ContinuationReceipt {
|
|
254
|
+
kind: 'continuation'; continuation_id: Id; native_session_id: string | null;
|
|
255
|
+
new_session_id: Id | null; verification: 'caller_reported' | 'connector_reported';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** HTTP operation names and bodies: exact paths/statuses/policies are in protocol.md. */
|
|
259
|
+
export interface Operations {
|
|
260
|
+
me: { request: null; response: Me };
|
|
261
|
+
teams_list: { request: { limit?: number; cursor?: string }; response: ServerPage<Team> };
|
|
262
|
+
teams_create: { request: { name: string }; response: Result<Team> };
|
|
263
|
+
list: { request: ListQuery; response: Page<Descriptor> };
|
|
264
|
+
search: { request: SearchQuery; response: SearchPage };
|
|
265
|
+
read: { request: ReadQuery; response: ReadResult };
|
|
266
|
+
follow: { request: FollowQuery; response: Event };
|
|
267
|
+
descriptor: { request: null; response: Result<Descriptor> };
|
|
268
|
+
access: { request: null; response: Result<Access> };
|
|
269
|
+
grant: { request: Omit<SessionGrant, 'id'>; response: Result<Access> };
|
|
270
|
+
invite: { request: CreateInvitation; response: Result<Invitation> };
|
|
271
|
+
accept_invite: { request: AcceptInvitation; response: { principal: Principal; credential: TokenCredential; team: Team } };
|
|
272
|
+
source_create: { request: CreateSource; response: Result<Source> };
|
|
273
|
+
source_config: { request: { format: 'otel-collector' }; response: Result<SourceCollectorConfig> };
|
|
274
|
+
enroll: { request: EnrollMachine; response: Result<Enrollment> };
|
|
275
|
+
machine_configuration: { request: null; response: Result<ConnectorConfiguration> };
|
|
276
|
+
heartbeat: { request: Heartbeat; response: Result<Machine> };
|
|
277
|
+
capture_begin: { request: CaptureBegin; response: Result<CaptureBeginResult> };
|
|
278
|
+
capture_part: { request: CapturePart; response: Result<{ next_part: number }> };
|
|
279
|
+
capture_commit: { request: CaptureCommit; response: Result<CaptureReceipt> };
|
|
280
|
+
correlate: { request: Omit<Correlation, 'id' | 'revision'>; response: Result<Descriptor> };
|
|
281
|
+
live_grant: { request: Omit<LiveGrant, 'id' | 'revision'>; response: Result<LiveGrant> };
|
|
282
|
+
machine_grant: { request: Pick<MachineGrant, 'subject_id' | 'caps' | 'targets' | 'expires_at'>; response: Result<MachineGrant> };
|
|
283
|
+
attach: { request: AttachmentRequest; response: Result<Attachment> };
|
|
284
|
+
prepare: { request: PrepareContinuation; response: Result<Continuation> };
|
|
285
|
+
launch: { request: null; response: Result<Operation> };
|
|
286
|
+
export: { request: ExportRequest; response: Result<Operation> };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export declare const ERROR_CODES: readonly ErrorCode[];
|
|
290
|
+
export declare const OPERATIONS: readonly (keyof Operations)[];
|
|
291
|
+
export declare class ContractError extends Error { code: 'invalid_request'; details: unknown; }
|
|
292
|
+
export declare function validateId(value: unknown, name?: string): string;
|
|
293
|
+
export declare function validateListQuery<T extends object>(value?: T): T;
|
|
294
|
+
export declare function validateSearchQuery<T extends object>(value: T): T;
|
|
295
|
+
export declare function validateReadQuery<T extends object>(value?: T): T;
|
|
296
|
+
export declare function validateNormalizedMessage<T extends NormalizedMessage>(value: T): T;
|
|
297
|
+
export declare function validateCaptureBegin<T extends CaptureBegin>(value: T): T;
|
|
298
|
+
export declare function validateCapturePart<T extends CapturePart>(value: T): T;
|
|
299
|
+
export declare function canonicalJson(value: unknown): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ContractError, canonicalJson, validationFailure } from '@volter/teams/server/wire';
|
|
2
|
+
export { ContractError, canonicalJson, validationFailure };
|
|
3
|
+
const ID = /^(srv|team|usr|svc|grp|ses|msg|src|mach|bind|cap|upload|grant|map|op|cont|inv|cred|req|root|inc|rule|att|evt|audit)_[0-9a-f]{32}$/;
|
|
4
|
+
export const ERROR_CODES = Object.freeze(['invalid_request','ambiguous_reference','authentication_required','not_found','forbidden','unsupported','machine_offline','controller_busy','conflict','resync_required','revision_mismatch','idempotency_conflict','source_revoked','source_paused','session_deleted','batch_gap','capture_conflict','fidelity_unavailable','rate_limited','request_too_large','internal_error']);
|
|
5
|
+
export const OPERATIONS = Object.freeze(['me','teams_list','teams_create','list','search','read','follow','descriptor','access','grant','invite','accept_invite','source_create','source_config','enroll','heartbeat','machine_configuration','capture_begin','capture_part','capture_commit','correlate','live_grant','attach','prepare','launch','export']);
|
|
6
|
+
const object = (value, name) => { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new ContractError(`${name} must be an object`); return value; };
|
|
7
|
+
const exact = (value, allowed, name) => { for (const key of Object.keys(object(value, name))) if (!allowed.includes(key)) throw new ContractError(`${name}.${key} is unknown`); };
|
|
8
|
+
const integer = (value, name, min = 0, max = Number.MAX_SAFE_INTEGER) => { if (!Number.isSafeInteger(value) || value < min || value > max) throw new ContractError(`${name} must be an integer from ${min} to ${max}`); };
|
|
9
|
+
const string = (value, name, { min = 1, max = 512 } = {}) => { if (typeof value !== 'string' || [...value].length < min || [...value].length > max) throw new ContractError(`${name} must be a string of ${min}..${max} code points`); };
|
|
10
|
+
const member = (value, values, name) => { if (!values.includes(value)) throw new ContractError(`${name} must be one of ${values.join(', ')}`); };
|
|
11
|
+
export function validateId(value, name = 'id') { if (typeof value !== 'string' || !ID.test(value)) throw new ContractError(`${name} is not a Teams ID`); return value; }
|
|
12
|
+
export function validateListQuery(value = {}) { exact(value, ['user','repo','harness','machine','source','since','until','limit','cursor','include_children'], 'query'); if (value.limit !== undefined) integer(value.limit, 'query.limit', 1, 200); if (value.include_children !== undefined && typeof value.include_children !== 'boolean') throw new ContractError('query.include_children must be boolean'); for (const k of ['since','until']) if (value[k] !== undefined) integer(value[k], `query.${k}`); return value; }
|
|
13
|
+
export function validateSearchQuery(value) { exact(value, ['query','user','repo','harness','machine','source','since','until','limit','cursor','include_children'], 'query'); validateListQuery(Object.fromEntries(Object.entries(value).filter(([k]) => k !== 'query'))); string(value.query?.trim(), 'query.query', { max: 512 }); return value; }
|
|
14
|
+
export function validateReadQuery(value = {}) { exact(value, ['tail','before','revision'], 'query'); if (value.tail !== undefined) integer(value.tail, 'query.tail', 1, 500); if (value.revision !== undefined) integer(value.revision, 'query.revision'); return value; }
|
|
15
|
+
export function validateNormalizedMessage(value) {
|
|
16
|
+
exact(value, ['role','content','tool_calls','tool_call_id','name','metadata'], 'message');
|
|
17
|
+
member(value.role, ['system','user','assistant','tool'], 'message.role');
|
|
18
|
+
if(value.content!==undefined&&typeof value.content!=='string'&&!Array.isArray(value.content))throw new ContractError('message.content must be a string or JSON content array');
|
|
19
|
+
if(value.tool_calls!==undefined){
|
|
20
|
+
if(!Array.isArray(value.tool_calls))throw new ContractError('message.tool_calls must be an array');
|
|
21
|
+
for(const [index,call] of value.tool_calls.entries()){
|
|
22
|
+
exact(call,['id','type','function'],`message.tool_calls[${index}]`);
|
|
23
|
+
string(call.id,`message.tool_calls[${index}].id`);
|
|
24
|
+
string(call.type,`message.tool_calls[${index}].type`);
|
|
25
|
+
exact(call.function,['name','arguments'],`message.tool_calls[${index}].function`);
|
|
26
|
+
string(call.function.name,`message.tool_calls[${index}].function.name`);
|
|
27
|
+
if(typeof call.function.arguments!=='string')throw new ContractError(`message.tool_calls[${index}].function.arguments must be a string`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if(value.tool_call_id!==undefined)string(value.tool_call_id,'message.tool_call_id');
|
|
31
|
+
if(value.name!==undefined)string(value.name,'message.name');
|
|
32
|
+
const metadata=object(value.metadata,'message.metadata');
|
|
33
|
+
for(const [key,item] of Object.entries(metadata))if(typeof item!=='string')throw new ContractError(`message.metadata.${key} must be a string`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function validateCaptureBegin(value) { exact(value, ['native_session_id','harness','generation','sequence','capture_id','previous_digest','visibility','rule_id','rule_revision'], 'capture'); string(value.native_session_id, 'capture.native_session_id', {max:512}); string(value.harness, 'capture.harness'); integer(value.generation,'capture.generation',1); integer(value.sequence,'capture.sequence',1); validateId(value.capture_id,'capture.capture_id'); if (value.previous_digest !== null) string(value.previous_digest,'capture.previous_digest',{min:64,max:64}); member(value.visibility,['team','restricted'],'capture.visibility'); if (value.rule_id !== null) validateId(value.rule_id,'capture.rule_id'); if ((value.rule_id === null) !== (value.rule_revision === null)) throw new ContractError('rule_id and rule_revision must both be null or present'); if (value.rule_revision !== null) integer(value.rule_revision,'capture.rule_revision',1); return value; }
|
|
37
|
+
export function validateCapturePart(value) { exact(value,['capture_id','index','messages'],'part'); validateId(value.capture_id,'part.capture_id'); integer(value.index,'part.index'); if (!Array.isArray(value.messages) || value.messages.length < 1 || value.messages.length > 500) throw new ContractError('part.messages must contain 1..500 messages'); for (const [i,m] of value.messages.entries()) { exact(m,['native_key','recorded_at','value'],`part.messages[${i}]`); string(m.native_key,`part.messages[${i}].native_key`,{max:512}); if (m.recorded_at !== null) integer(m.recorded_at,`part.messages[${i}].recorded_at`); validateNormalizedMessage(m.value); } return value; }
|
package/execution.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Remote execution on this machine: the `exec` and `sftp` streams an operator opens to run a
|
|
2
|
+
// pilot's tool processes here and to mount this machine's files, and the `exec-server` stream a
|
|
3
|
+
// harness with its own execution door runs its tools through (docs/plans/remote-execution.md).
|
|
4
|
+
//
|
|
5
|
+
// The daemon owns every job it starts. Where this user holds a delegated cgroup v2 subtree, each
|
|
6
|
+
// job runs in its own cgroup and ending it writes `cgroup.kill`, which takes descendants that
|
|
7
|
+
// called setsid() with it; elsewhere a job is its process group, and the capability report says
|
|
8
|
+
// so. A job ends when its stream closes, when the daemon stops, and, if the daemon died, when
|
|
9
|
+
// the next daemon reaps the record it left.
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { delimiter, join } from 'node:path';
|
|
13
|
+
import { EXECUTION_DOORS, executionDoorFor } from '@volter/supercode-harness-sdk/execution-doors';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
const JOB_EXEC = fileURLToPath(new URL('./remote/job-exec.py', import.meta.url));
|
|
18
|
+
const SFTP_SERVERS = ['/usr/lib/openssh/sftp-server', '/usr/libexec/openssh/sftp-server', '/usr/libexec/sftp-server', '/usr/lib/sftp-server'];
|
|
19
|
+
|
|
20
|
+
/** The cgroup v2 directory this user may create job cgroups under, or null. */
|
|
21
|
+
export function delegatedCgroup(env = process.env) {
|
|
22
|
+
const named = env.SUPERCODE_TEAMS_CGROUP;
|
|
23
|
+
const candidates = named ? [named] : ['/sys/fs/cgroup/supercode-machine'];
|
|
24
|
+
for (const dir of candidates) {
|
|
25
|
+
try {
|
|
26
|
+
const stat = statSync(dir);
|
|
27
|
+
if (!stat.isDirectory() || (typeof process.getuid === 'function' && stat.uid !== process.getuid())) continue;
|
|
28
|
+
if (!existsSync(join(dir, 'cgroup.procs')) || !existsSync(join(dir, 'cgroup.kill'))) continue;
|
|
29
|
+
return dir;
|
|
30
|
+
} catch { /* not this one */ }
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sftpServer() {
|
|
36
|
+
return SFTP_SERVERS.find((path) => existsSync(path)) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `program` as this daemon's PATH finds it, or null. */
|
|
40
|
+
function onPath(program, env) {
|
|
41
|
+
for (const dir of (env.PATH ?? '').split(delimiter)) if (dir && existsSync(join(dir, program))) return join(dir, program);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The harnesses whose own execution server this machine can run (execution-doors.mjs). */
|
|
46
|
+
export function execServers(env = process.env) {
|
|
47
|
+
return Object.entries(EXECUTION_DOORS).filter(([, door]) => door.door === 'exec-server' && onPath(door.serve.program, env)).map(([harness]) => harness);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What this machine can do as an execution target, for `describe`. */
|
|
51
|
+
export function executionCapabilities(env = process.env) {
|
|
52
|
+
const python = ['/usr/bin/python3', '/usr/local/bin/python3'].some((path) => existsSync(path));
|
|
53
|
+
return {
|
|
54
|
+
exec: process.platform === 'linux' && python,
|
|
55
|
+
sftp: Boolean(sftpServer()),
|
|
56
|
+
exec_servers: execServers(env),
|
|
57
|
+
jobs: delegatedCgroup(env) ? 'cgroup' : 'process-group',
|
|
58
|
+
unsupported: ['named-unix-sockets', 'shared-memory', 'descriptor-passing', 'datagrams'],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class JobOwner {
|
|
63
|
+
constructor({ home, env = process.env, log = () => {} }) {
|
|
64
|
+
this.dir = join(home, 'jobs');
|
|
65
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
66
|
+
this.env = env;
|
|
67
|
+
this.log = log;
|
|
68
|
+
this.cgroup = delegatedCgroup(env);
|
|
69
|
+
this.live = new Map();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** End every job a previous daemon left running. */
|
|
73
|
+
reapOrphans() {
|
|
74
|
+
for (const name of readdirSync(this.dir)) {
|
|
75
|
+
if (!name.endsWith('.json')) continue;
|
|
76
|
+
let record;
|
|
77
|
+
try { record = JSON.parse(readFileSync(join(this.dir, name), 'utf8')); } catch { record = null; }
|
|
78
|
+
if (record) this.#end(record, 'daemon restarted');
|
|
79
|
+
rmSync(join(this.dir, name), { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run `program args` as one owned job with its stdio carried by `stream` (base64 both ways).
|
|
85
|
+
* The child moves itself into its cgroup before it execs, so nothing it forks is outside.
|
|
86
|
+
*/
|
|
87
|
+
run(stream, { kind, program, args, cwd }) {
|
|
88
|
+
const id = randomBytes(16).toString('hex');
|
|
89
|
+
if (kind === 'exec') args = [...args, id, join(this.dir, 'state')];
|
|
90
|
+
const group = this.cgroup ? join(this.cgroup, `job-${id}`) : null;
|
|
91
|
+
if (group) mkdirSync(group);
|
|
92
|
+
const argv = group
|
|
93
|
+
? ['/bin/sh', '-c', 'echo $$ > "$0/cgroup.procs" && exec "$@"', group, program, ...args]
|
|
94
|
+
: [program, ...args];
|
|
95
|
+
const child = spawn(argv[0], argv.slice(1), { cwd, detached: true, stdio: ['pipe', 'pipe', 'inherit'] });
|
|
96
|
+
const record = { id, kind, pid: child.pid, cgroup: group, at: Date.now() };
|
|
97
|
+
writeFileSync(join(this.dir, `${id}.json`), JSON.stringify(record), { mode: 0o600 });
|
|
98
|
+
this.live.set(id, record);
|
|
99
|
+
child.stdout.on('data', (bytes) => { if (!stream.closed) stream.write(bytes.toString('base64')); });
|
|
100
|
+
stream.on('data', (chunk) => { if (!child.stdin.destroyed) child.stdin.write(Buffer.from(chunk, 'base64')); });
|
|
101
|
+
const finish = (reason) => {
|
|
102
|
+
if (!this.live.has(id)) return;
|
|
103
|
+
this.live.delete(id);
|
|
104
|
+
this.#end(record, reason);
|
|
105
|
+
rmSync(join(this.dir, `${id}.json`), { force: true });
|
|
106
|
+
if (!stream.closed) stream.close(reason);
|
|
107
|
+
};
|
|
108
|
+
stream.on('close', () => finish('stream closed'));
|
|
109
|
+
child.on('exit', () => {
|
|
110
|
+
// The endpoint has returned: whatever it left running goes with it.
|
|
111
|
+
child.stdout.once('end', () => finish('exited'));
|
|
112
|
+
if (child.stdout.readableEnded) finish('exited');
|
|
113
|
+
});
|
|
114
|
+
child.on('error', (error) => finish(error.message));
|
|
115
|
+
return record;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
stopAll(reason = 'daemon stopped') {
|
|
119
|
+
for (const record of [...this.live.values()]) this.#end(record, reason);
|
|
120
|
+
this.live.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#end(record, reason) {
|
|
124
|
+
if (record.cgroup) {
|
|
125
|
+
try { writeFileSync(join(record.cgroup, 'cgroup.kill'), '1'); } catch { /* already gone */ }
|
|
126
|
+
// The kernel empties the cgroup asynchronously; remove it once it is.
|
|
127
|
+
const remove = (tries) => {
|
|
128
|
+
try { rmdirSync(record.cgroup); } catch { if (tries > 0) setTimeout(() => remove(tries - 1), 100).unref?.(); }
|
|
129
|
+
};
|
|
130
|
+
remove(50);
|
|
131
|
+
} else if (record.pid) {
|
|
132
|
+
try { process.kill(-record.pid, 'SIGKILL'); } catch { /* already gone */ }
|
|
133
|
+
}
|
|
134
|
+
this.log({ job: record.id, kind: record.kind, cgroup: Boolean(record.cgroup), outcome: 'ended', reason });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The endpoints the `exec`, `sftp` and `exec-server` streams run. */
|
|
139
|
+
export function streamProgram(name, params = {}, env = process.env) {
|
|
140
|
+
if (name === 'exec-server') {
|
|
141
|
+
const door = executionDoorFor(String(params.harness ?? ''));
|
|
142
|
+
if (door.door !== 'exec-server') return null;
|
|
143
|
+
const program = onPath(door.serve.program, env);
|
|
144
|
+
return program ? { program, args: [...door.serve.args] } : null;
|
|
145
|
+
}
|
|
146
|
+
if (name === 'sftp') {
|
|
147
|
+
const server = sftpServer();
|
|
148
|
+
return server ? { program: server, args: ['-e'] } : null;
|
|
149
|
+
}
|
|
150
|
+
if (name === 'exec') return { program: 'python3', args: [JOB_EXEC] };
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A project moving between machines (docs/plans/remote-execution.md, decision 4). `export`
|
|
156
|
+
* streams `tar` of `path` once the caller writes `{"start": true}`; `import` writes what arrives into a staging directory beside `path`
|
|
157
|
+
* and publishes it (a rename) only when the sender's `{"commit": sha256}` names exactly the
|
|
158
|
+
* bytes received and `tar` accepted them, answering with the close reason. Anything else (a
|
|
159
|
+
* lost stream, a mismatch, an existing `path`) leaves `path` untouched and removes the staging.
|
|
160
|
+
*/
|
|
161
|
+
export function serveWorkspace(stream, { op, path }, { onEnd = () => {} } = {}) {
|
|
162
|
+
if (typeof path !== 'string' || !path.startsWith('/')) return stream.close('workspace path must be absolute');
|
|
163
|
+
if (op === 'export') {
|
|
164
|
+
if (!existsSync(path)) return stream.close(`no workspace at ${path}`);
|
|
165
|
+
// Nothing is sent before the caller's {start: true}: its listeners are attached by then, and
|
|
166
|
+
// a small project's every frame would otherwise race its open.
|
|
167
|
+
stream.once('data', () => {
|
|
168
|
+
const tar = spawn('tar', ['-C', path, '-cf', '-', '.'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
169
|
+
tar.stdout.on('data', (bytes) => { if (!stream.closed) stream.write(bytes.toString('base64')); });
|
|
170
|
+
stream.on('close', () => tar.kill('SIGKILL'));
|
|
171
|
+
tar.on('close', (code) => { onEnd(code === 0 ? 'exported' : `tar exited ${code}`); if (!stream.closed) stream.close(code === 0 ? 'end' : `tar exited ${code}`); });
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (op !== 'import') return stream.close('workspace op is export or import');
|
|
176
|
+
if (existsSync(path)) return stream.close(`discarded (${path} exists; a handoff never writes over a workspace)`);
|
|
177
|
+
const staging = `${path}.incoming-${randomBytes(6).toString('hex')}`;
|
|
178
|
+
mkdirSync(staging, { recursive: true, mode: 0o700 });
|
|
179
|
+
const hash = createHash('sha256');
|
|
180
|
+
const tar = spawn('tar', ['-C', staging, '-xf', '-'], { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
181
|
+
tar.stdin.on('error', () => {}); // tar that exited early is answered at commit, not by a crash
|
|
182
|
+
const extracted = new Promise((resolveExtract) => tar.on('close', (code) => resolveExtract(code)));
|
|
183
|
+
let settled = false;
|
|
184
|
+
const settle = (outcome) => {
|
|
185
|
+
if (settled) return;
|
|
186
|
+
settled = true;
|
|
187
|
+
if (!outcome.startsWith('published')) rmSync(staging, { recursive: true, force: true });
|
|
188
|
+
onEnd(outcome);
|
|
189
|
+
if (!stream.closed) stream.close(outcome);
|
|
190
|
+
};
|
|
191
|
+
// Data frames are base64; the one frame that is not is the sender's commit, answered by the
|
|
192
|
+
// close reason: `published <sha256>` or `discarded (why)`.
|
|
193
|
+
stream.on('data', async (chunk) => {
|
|
194
|
+
if (!chunk.startsWith('{')) { const bytes = Buffer.from(chunk, 'base64'); hash.update(bytes); tar.stdin.write(bytes); return; }
|
|
195
|
+
let commit = null;
|
|
196
|
+
try { commit = JSON.parse(chunk).commit; } catch { /* not a commit */ }
|
|
197
|
+
tar.stdin.end();
|
|
198
|
+
const code = await extracted;
|
|
199
|
+
const digest = hash.digest('hex');
|
|
200
|
+
if (code !== 0) return settle(`discarded (tar exited ${code})`);
|
|
201
|
+
if (commit !== digest) return settle(`discarded (sender committed ${commit}, received ${digest})`);
|
|
202
|
+
if (existsSync(path)) return settle(`discarded (${path} appeared)`);
|
|
203
|
+
try { renameSync(staging, path); settle(`published ${digest}`); } catch (error) { settle(`discarded (${error.message})`); }
|
|
204
|
+
});
|
|
205
|
+
stream.on('close', () => { tar.stdin.end(); settle('discarded (the sender left before committing)'); });
|
|
206
|
+
}
|
package/fleet.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The fleet arms of supercode's own verbs: `discover --fleet|--on`,
|
|
2
|
+
// `open --on`, and `harness drive --on`. A machine named with `--on` is an
|
|
3
|
+
// enrolled machine of the selected Teams context, reached through its server;
|
|
4
|
+
// without `--on` the verb acts on this OS user's own daemon.
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
7
|
+
import { harnessTransport, nodeRpc, openRemoteMachine } from './client.mjs';
|
|
8
|
+
import { openMachine } from './machine-door.mjs';
|
|
9
|
+
import { WorkspaceContexts, fromContext } from './context.mjs';
|
|
10
|
+
import { teamsHome } from '@volter/teams/home';
|
|
11
|
+
|
|
12
|
+
/** Open the channel to the machine `name` names in `context` (or this box's daemon when name is null). */
|
|
13
|
+
export function openOn(name = null, { context = null, env = process.env } = {}) {
|
|
14
|
+
return openMachine({ machine: name, context, env });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One `harness.v1` request over a channel's operator stream. The harness SDK
|
|
19
|
+
* runs unchanged: the channel is just its transport.
|
|
20
|
+
*/
|
|
21
|
+
export async function harnessRequest(channel, method, params = {}) {
|
|
22
|
+
const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
|
|
23
|
+
try {
|
|
24
|
+
await client.start?.();
|
|
25
|
+
return await client.request(qualify(method), params);
|
|
26
|
+
} finally {
|
|
27
|
+
await client.close?.();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `sessions.discover` → `harness.v1.sessions.discover`; an already-qualified method is left alone. */
|
|
32
|
+
export const qualify = (method) => (String(method).startsWith('harness.v1.') ? method : `harness.v1.${method}`);
|
|
33
|
+
|
|
34
|
+
/** One node-door call (`launch`, `panes.list`, …) over a channel. */
|
|
35
|
+
export async function nodeRequest(channel, method, params = {}) {
|
|
36
|
+
const rpc = await nodeRpc(channel);
|
|
37
|
+
return rpc.call(method, params);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run `harness.v1.sessions.discover` on every machine of the selected context
|
|
42
|
+
* the caller can reach (or the named subset) and merge the answers into one
|
|
43
|
+
* page: each session carries the `machine` it was found on, and each machine
|
|
44
|
+
* that refused or is offline carries its reason. Nothing is stored.
|
|
45
|
+
*/
|
|
46
|
+
export async function fleetDiscover({ context = null, machines = null, params = {}, env = process.env } = {}) {
|
|
47
|
+
const home = teamsHome(env);
|
|
48
|
+
const supercodeHome = env.SUPERCODE_HOME ?? resolve(home, '..');
|
|
49
|
+
const contexts = new WorkspaceContexts(supercodeHome);
|
|
50
|
+
const name = context ?? env.SUPERCODE_CONTEXT ?? contexts.read().current;
|
|
51
|
+
if (!name || name === 'local') throw new Error('--fleet discovers the machines of a Teams context: `supercode teams login …`, then `supercode context use NAME` or --context NAME');
|
|
52
|
+
const { client, context: selected } = fromContext({ supercodeHome, name, fetch: globalThis.fetch });
|
|
53
|
+
const token = contexts.credential(selected.credential_ref).token;
|
|
54
|
+
const listed = (await client.team(selected.team_id).machines.list()).items.filter((item) => item.enrollment === 'active');
|
|
55
|
+
const targets = machines && machines.length
|
|
56
|
+
? machines.map((wanted) => listed.find((item) => item.name === wanted || item.id === wanted) ?? { name: wanted, id: null })
|
|
57
|
+
: listed;
|
|
58
|
+
const results = await Promise.all(targets.map(async (machine) => {
|
|
59
|
+
if (!machine.id) return { name: machine.name, error: 'not an accessible machine of this team' };
|
|
60
|
+
let channel = null;
|
|
61
|
+
try {
|
|
62
|
+
channel = await openRemoteMachine({ origin: selected.origin, teamId: selected.team_id, machineId: machine.id, token });
|
|
63
|
+
const page = await harnessRequest(channel, 'harness.v1.sessions.discover', params);
|
|
64
|
+
return { name: machine.name, sessions: page?.sessions ?? [] };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return { name: machine.name, error: error.message ?? String(error) };
|
|
67
|
+
} finally {
|
|
68
|
+
channel?.close();
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
const sessions = [];
|
|
72
|
+
const unreachable = [];
|
|
73
|
+
for (const result of results) {
|
|
74
|
+
if (result.error) unreachable.push({ machine: result.name, error: result.error });
|
|
75
|
+
else for (const session of result.sessions) sessions.push({ machine: result.name, ...session });
|
|
76
|
+
}
|
|
77
|
+
return { sessions, unreachable };
|
|
78
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @volter/supercode-teams: a machine daemon that serves this machine's
|
|
2
|
+
// doors, and a Teams server that shares sessions and routes callers to enrolled
|
|
3
|
+
// machines. Design: docs/plans/teams-server.md.
|
|
4
|
+
export { MachineCore } from '@volter/teams/machine-core';
|
|
5
|
+
export { TeamsMachine } from './machine.mjs';
|
|
6
|
+
export { openLocalMachine, openRemoteMachine, harnessTransport, execServerBridge, nodeRpc, attachPane, attachRuntime, forwardPort, watchMachine, StreamRpc } from './client.mjs';
|
|
7
|
+
export { openMachine } from './machine-door.mjs';
|
|
8
|
+
export { Channel, ChannelStream, NODE_PROTOCOL } from '@volter/teams/channel';
|
|
9
|
+
export { holds, targetsCover, admitFromGrants, LOCAL_OPERATOR } from '@volter/teams/admission';
|
|
10
|
+
export { generateKeyPair, loadOrCreateKeyPair, publicKeyId, publicKeyPemFromId, fingerprint, signBytes, verifyBytes, canonical } from '@volter/teams/identity';
|
|
11
|
+
export { teamsHome, supercodeHome, paths, readLimits, DEFAULT_LIMITS, AccountLog, Launches } from '@volter/teams/home';
|
|
12
|
+
export { computeLabel, rankOf, notifies, judge, normalizeRank, DEFAULT_RANK, DEFAULT_NOTIFY, DEFAULT_STUCK_IDLE_SECONDS, LABELS, LIVENESS } from './attention.mjs';
|