@tagma/sdk 0.7.3 → 0.7.5
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 +85 -57
- package/dist/approval.d.ts +2 -12
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +1 -90
- package/dist/approval.js.map +1 -1
- package/dist/bootstrap.d.ts +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/completions/file-exists.js +1 -1
- package/dist/completions/file-exists.js.map +1 -1
- package/dist/completions/output-check.d.ts.map +1 -1
- package/dist/completions/output-check.js +17 -4
- package/dist/completions/output-check.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.d.ts.map +1 -0
- package/dist/dataflow.js +2 -0
- package/dist/dataflow.js.map +1 -0
- package/dist/drivers/opencode.d.ts.map +1 -1
- package/dist/drivers/opencode.js +23 -71
- package/dist/drivers/opencode.js.map +1 -1
- package/dist/engine.d.ts +5 -56
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +7 -297
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +2 -60
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +1 -153
- package/dist/logger.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +1 -2
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +2 -2
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +1 -1
- package/dist/plugins.js.map +1 -1
- package/dist/runner.d.ts +1 -35
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +1 -610
- package/dist/runner.js.map +1 -1
- package/dist/runtime/adapters/stdin-approval.d.ts +2 -0
- package/dist/runtime/adapters/stdin-approval.d.ts.map +1 -0
- package/dist/runtime/adapters/stdin-approval.js +2 -0
- package/dist/runtime/adapters/stdin-approval.js.map +1 -0
- package/dist/runtime/adapters/websocket-approval.d.ts +2 -0
- package/dist/runtime/adapters/websocket-approval.d.ts.map +1 -0
- package/dist/runtime/adapters/websocket-approval.js +2 -0
- package/dist/runtime/adapters/websocket-approval.js.map +1 -0
- package/dist/runtime/bun-process-runner.d.ts +2 -0
- package/dist/runtime/bun-process-runner.d.ts.map +1 -0
- package/dist/runtime/bun-process-runner.js +2 -0
- package/dist/runtime/bun-process-runner.js.map +1 -0
- package/dist/runtime.d.ts +2 -8
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -7
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -4
- package/dist/schema.js.map +1 -1
- package/dist/tagma.d.ts +3 -4
- package/dist/tagma.d.ts.map +1 -1
- package/dist/tagma.js +2 -3
- package/dist/tagma.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +74 -108
- package/dist/triggers/file.js.map +1 -1
- package/dist/triggers/manual.d.ts.map +1 -1
- package/dist/triggers/manual.js +1 -2
- package/dist/triggers/manual.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -12
- package/dist/types.js.map +1 -1
- package/dist/utils-api.d.ts +1 -1
- package/dist/utils-api.d.ts.map +1 -1
- package/dist/utils-api.js +1 -1
- package/dist/utils-api.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +5 -12
- package/dist/validate-raw.js.map +1 -1
- package/package.json +20 -22
- package/dist/adapters/stdin-approval.d.ts +0 -6
- package/dist/adapters/stdin-approval.d.ts.map +0 -1
- package/dist/adapters/stdin-approval.js +0 -90
- package/dist/adapters/stdin-approval.js.map +0 -1
- package/dist/adapters/websocket-approval.d.ts +0 -28
- package/dist/adapters/websocket-approval.d.ts.map +0 -1
- package/dist/adapters/websocket-approval.js +0 -147
- package/dist/adapters/websocket-approval.js.map +0 -1
- package/dist/core/dataflow.d.ts +0 -23
- package/dist/core/dataflow.d.ts.map +0 -1
- package/dist/core/dataflow.js +0 -99
- package/dist/core/dataflow.js.map +0 -1
- package/dist/core/log-prune.d.ts +0 -16
- package/dist/core/log-prune.d.ts.map +0 -1
- package/dist/core/log-prune.js +0 -34
- package/dist/core/log-prune.js.map +0 -1
- package/dist/core/preflight.d.ts +0 -13
- package/dist/core/preflight.d.ts.map +0 -1
- package/dist/core/preflight.js +0 -61
- package/dist/core/preflight.js.map +0 -1
- package/dist/core/run-context.d.ts +0 -55
- package/dist/core/run-context.d.ts.map +0 -1
- package/dist/core/run-context.js +0 -158
- package/dist/core/run-context.js.map +0 -1
- package/dist/core/run-state.d.ts +0 -25
- package/dist/core/run-state.d.ts.map +0 -1
- package/dist/core/run-state.js +0 -93
- package/dist/core/run-state.js.map +0 -1
- package/dist/core/scheduler.d.ts +0 -13
- package/dist/core/scheduler.d.ts.map +0 -1
- package/dist/core/scheduler.js +0 -35
- package/dist/core/scheduler.js.map +0 -1
- package/dist/core/task-executor.d.ts +0 -13
- package/dist/core/task-executor.d.ts.map +0 -1
- package/dist/core/task-executor.js +0 -601
- package/dist/core/task-executor.js.map +0 -1
- package/dist/core/trigger-errors.d.ts +0 -9
- package/dist/core/trigger-errors.d.ts.map +0 -1
- package/dist/core/trigger-errors.js +0 -15
- package/dist/core/trigger-errors.js.map +0 -1
- package/dist/dag.d.ts +0 -45
- package/dist/dag.d.ts.map +0 -1
- package/dist/dag.js +0 -177
- package/dist/dag.js.map +0 -1
- package/dist/hooks.d.ts +0 -73
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js +0 -106
- package/dist/hooks.js.map +0 -1
- package/dist/pipeline-definition.d.ts +0 -3
- package/dist/pipeline-definition.d.ts.map +0 -1
- package/dist/pipeline-definition.js +0 -4
- package/dist/pipeline-definition.js.map +0 -1
- package/dist/ports.d.ts +0 -196
- package/dist/ports.d.ts.map +0 -1
- package/dist/ports.js +0 -688
- package/dist/ports.js.map +0 -1
- package/dist/prompt-doc.d.ts +0 -70
- package/dist/prompt-doc.d.ts.map +0 -1
- package/dist/prompt-doc.js +0 -154
- package/dist/prompt-doc.js.map +0 -1
- package/dist/registry.d.ts +0 -67
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -293
- package/dist/registry.js.map +0 -1
- package/dist/task-ref.d.ts +0 -55
- package/dist/task-ref.d.ts.map +0 -1
- package/dist/task-ref.js +0 -103
- package/dist/task-ref.js.map +0 -1
- package/dist/utils.d.ts +0 -13
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -177
- package/dist/utils.js.map +0 -1
- package/src/adapters/stdin-approval.ts +0 -106
- package/src/adapters/websocket-approval.ts +0 -224
- package/src/approval.ts +0 -131
- package/src/bootstrap.ts +0 -55
- package/src/completions/exit-code.ts +0 -34
- package/src/completions/file-exists.ts +0 -66
- package/src/completions/output-check.test.ts +0 -50
- package/src/completions/output-check.ts +0 -92
- package/src/config-ops.test.ts +0 -70
- package/src/config-ops.ts +0 -328
- package/src/config.ts +0 -26
- package/src/core/dataflow.test.ts +0 -166
- package/src/core/dataflow.ts +0 -161
- package/src/core/log-prune.test.ts +0 -58
- package/src/core/log-prune.ts +0 -43
- package/src/core/preflight.test.ts +0 -49
- package/src/core/preflight.ts +0 -89
- package/src/core/run-context.test.ts +0 -256
- package/src/core/run-context.ts +0 -211
- package/src/core/run-state.test.ts +0 -98
- package/src/core/run-state.ts +0 -122
- package/src/core/scheduler.test.ts +0 -83
- package/src/core/scheduler.ts +0 -42
- package/src/core/task-executor.ts +0 -743
- package/src/core/trigger-errors.ts +0 -15
- package/src/dag.test.ts +0 -56
- package/src/dag.ts +0 -245
- package/src/drivers/opencode.ts +0 -410
- package/src/engine-ports-mixed.test.ts +0 -156
- package/src/engine-ports.test.ts +0 -166
- package/src/engine-task-type.test.ts +0 -56
- package/src/engine.ts +0 -458
- package/src/hooks.ts +0 -193
- package/src/index.ts +0 -33
- package/src/logger.ts +0 -182
- package/src/middlewares/static-context.ts +0 -49
- package/src/pipeline-definition.ts +0 -5
- package/src/pipeline-runner.test.ts +0 -91
- package/src/pipeline-runner.ts +0 -194
- package/src/plugin-registry.test.ts +0 -382
- package/src/plugins.ts +0 -21
- package/src/ports.test.ts +0 -678
- package/src/ports.ts +0 -925
- package/src/prompt-doc.test.ts +0 -174
- package/src/prompt-doc.ts +0 -169
- package/src/registry.ts +0 -353
- package/src/runner.test.ts +0 -142
- package/src/runner.ts +0 -666
- package/src/runtime.ts +0 -20
- package/src/schema-ports.test.ts +0 -172
- package/src/schema.test.ts +0 -213
- package/src/schema.ts +0 -379
- package/src/tagma.test.ts +0 -155
- package/src/tagma.ts +0 -62
- package/src/task-ref.test.ts +0 -401
- package/src/task-ref.ts +0 -121
- package/src/triggers/file.ts +0 -164
- package/src/triggers/manual.ts +0 -86
- package/src/types.ts +0 -18
- package/src/utils-api.ts +0 -8
- package/src/utils.test.ts +0 -28
- package/src/utils.ts +0 -203
- package/src/validate-raw-plugin-types.test.ts +0 -60
- package/src/validate-raw-ports.test.ts +0 -136
- package/src/validate-raw.ts +0 -852
- package/src/yaml-compiler.test.ts +0 -108
- package/src/yaml-compiler.ts +0 -110
- package/src/yaml.ts +0 -11
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import type { ApprovalGateway, ApprovalEvent } from '../approval';
|
|
2
|
-
|
|
3
|
-
// ═══ WebSocket Approval Adapter ═══
|
|
4
|
-
//
|
|
5
|
-
// Bridges the ApprovalGateway to WebSocket clients (e.g. a frontend UI).
|
|
6
|
-
// Mirrors the stdin-approval adapter pattern: subscribe to gateway events,
|
|
7
|
-
// forward them as JSON to all connected clients, and call gateway.resolve()
|
|
8
|
-
// when a client sends a resolution message.
|
|
9
|
-
//
|
|
10
|
-
// Protocol — server → client:
|
|
11
|
-
// { type: 'pending', requests: ApprovalRequest[] } ← sent on connect
|
|
12
|
-
// { type: 'approval_requested', request: ApprovalRequest }
|
|
13
|
-
// { type: 'approval_resolved', request: ApprovalRequest, decision: ApprovalDecision }
|
|
14
|
-
// { type: 'approval_expired', request: ApprovalRequest }
|
|
15
|
-
// { type: 'approval_aborted', request: ApprovalRequest, reason: string }
|
|
16
|
-
//
|
|
17
|
-
// Protocol — client → server:
|
|
18
|
-
// { type: 'resolve', approvalId: string, outcome: 'approved'|'rejected',
|
|
19
|
-
// actor?: string, reason?: string }
|
|
20
|
-
|
|
21
|
-
export interface WebSocketApprovalAdapterOptions {
|
|
22
|
-
port?: number; // default: 3000
|
|
23
|
-
hostname?: string; // default: 'localhost'
|
|
24
|
-
/**
|
|
25
|
-
* M11: shared secret required from the client during the WebSocket
|
|
26
|
-
* upgrade. The token can be supplied either as the `?token=` query
|
|
27
|
-
* parameter or in the `x-tagma-token` request header. When set, any
|
|
28
|
-
* upgrade request that fails the check is rejected with HTTP 401 and
|
|
29
|
-
* never reaches the WebSocket layer (so a misconfigured client cannot
|
|
30
|
-
* exhaust rate-limit slots either). Leave undefined for backward
|
|
31
|
-
* compatibility with localhost-only deployments.
|
|
32
|
-
*/
|
|
33
|
-
token?: string;
|
|
34
|
-
/**
|
|
35
|
-
* M11: opt-out of origin checking. Defaults to false, meaning Origin
|
|
36
|
-
* headers are restricted to loopback hosts (localhost / 127.0.0.1 / ::1).
|
|
37
|
-
* Requests without an Origin header are still allowed so non-browser local
|
|
38
|
-
* clients can connect. Set true only for trusted reverse-proxy setups.
|
|
39
|
-
*/
|
|
40
|
-
allowAnyOrigin?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface WebSocketApprovalAdapter {
|
|
44
|
-
readonly port: number;
|
|
45
|
-
readonly detach: () => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Maximum allowed message payload (bytes) to prevent DoS via oversized messages.
|
|
49
|
-
const MAX_PAYLOAD_BYTES = 4_096;
|
|
50
|
-
// Per-client rate limit: at most this many messages per window.
|
|
51
|
-
const RATE_LIMIT_MAX = 10;
|
|
52
|
-
const RATE_LIMIT_WINDOW_MS = 1_000;
|
|
53
|
-
|
|
54
|
-
export function attachWebSocketApprovalAdapter(
|
|
55
|
-
gateway: ApprovalGateway,
|
|
56
|
-
options: WebSocketApprovalAdapterOptions = {},
|
|
57
|
-
): WebSocketApprovalAdapter {
|
|
58
|
-
const port = options.port ?? 3000;
|
|
59
|
-
const hostname = options.hostname ?? 'localhost';
|
|
60
|
-
const requiredToken = options.token ?? null;
|
|
61
|
-
const enforceOriginCheck = options.allowAnyOrigin !== true;
|
|
62
|
-
|
|
63
|
-
function isLoopbackOrigin(origin: string): boolean {
|
|
64
|
-
try {
|
|
65
|
-
const host = new URL(origin).hostname.toLowerCase();
|
|
66
|
-
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
type WS = import('bun').ServerWebSocket<unknown>;
|
|
73
|
-
const clients = new Set<WS>();
|
|
74
|
-
const clientRates = new Map<WS, { count: number; resetAt: number }>();
|
|
75
|
-
|
|
76
|
-
function broadcast(msg: unknown): void {
|
|
77
|
-
const text = JSON.stringify(msg);
|
|
78
|
-
for (const ws of clients) {
|
|
79
|
-
ws.send(text);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const unsubscribe = gateway.subscribe((event: ApprovalEvent) => {
|
|
84
|
-
switch (event.type) {
|
|
85
|
-
case 'requested':
|
|
86
|
-
broadcast({ type: 'approval_requested', request: event.request });
|
|
87
|
-
break;
|
|
88
|
-
case 'resolved':
|
|
89
|
-
broadcast({ type: 'approval_resolved', request: event.request, decision: event.decision });
|
|
90
|
-
break;
|
|
91
|
-
case 'expired':
|
|
92
|
-
broadcast({ type: 'approval_expired', request: event.request });
|
|
93
|
-
break;
|
|
94
|
-
case 'aborted':
|
|
95
|
-
broadcast({ type: 'approval_aborted', request: event.request, reason: event.reason });
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const server = Bun.serve({
|
|
101
|
-
port,
|
|
102
|
-
hostname,
|
|
103
|
-
|
|
104
|
-
fetch(req, server) {
|
|
105
|
-
if (enforceOriginCheck) {
|
|
106
|
-
const origin = req.headers.get('origin');
|
|
107
|
-
if (origin && !isLoopbackOrigin(origin)) {
|
|
108
|
-
return new Response('forbidden origin', { status: 403 });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// M11: enforce token before any upgrade so an unauthenticated client
|
|
112
|
-
// can't even open a socket. Tokens may arrive via header or query.
|
|
113
|
-
if (requiredToken !== null) {
|
|
114
|
-
const headerToken = req.headers.get('x-tagma-token') ?? '';
|
|
115
|
-
let queryToken = '';
|
|
116
|
-
try {
|
|
117
|
-
queryToken = new URL(req.url).searchParams.get('token') ?? '';
|
|
118
|
-
} catch {
|
|
119
|
-
/* malformed URL — leave queryToken empty */
|
|
120
|
-
}
|
|
121
|
-
const presented = headerToken || queryToken;
|
|
122
|
-
if (presented !== requiredToken) {
|
|
123
|
-
return new Response('unauthorized', { status: 401 });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (server.upgrade(req)) return undefined;
|
|
127
|
-
return new Response('tagma-sdk WebSocket approval endpoint', { status: 426 });
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
websocket: {
|
|
131
|
-
open(ws) {
|
|
132
|
-
clients.add(ws);
|
|
133
|
-
// Sync current pending approvals to newly connected client.
|
|
134
|
-
ws.send(JSON.stringify({ type: 'pending', requests: gateway.pending() }));
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
message(ws, raw) {
|
|
138
|
-
const rawStr = typeof raw === 'string' ? raw : raw.toString();
|
|
139
|
-
|
|
140
|
-
// Payload size guard — reject oversized messages before parsing.
|
|
141
|
-
if (rawStr.length > MAX_PAYLOAD_BYTES) {
|
|
142
|
-
ws.send(JSON.stringify({ type: 'error', message: 'message too large' }));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Per-client rate limit.
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
const rate = clientRates.get(ws) ?? { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
|
|
149
|
-
if (now >= rate.resetAt) {
|
|
150
|
-
rate.count = 0;
|
|
151
|
-
rate.resetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
152
|
-
}
|
|
153
|
-
rate.count++;
|
|
154
|
-
clientRates.set(ws, rate);
|
|
155
|
-
if (rate.count > RATE_LIMIT_MAX) {
|
|
156
|
-
ws.send(JSON.stringify({ type: 'error', message: 'rate limit exceeded' }));
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let msg: unknown;
|
|
161
|
-
try {
|
|
162
|
-
msg = JSON.parse(rawStr);
|
|
163
|
-
} catch {
|
|
164
|
-
ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!isResolveMessage(msg)) {
|
|
169
|
-
ws.send(JSON.stringify({ type: 'error', message: 'unknown message type' }));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const ok = gateway.resolve(msg.approvalId, {
|
|
174
|
-
outcome: msg.outcome,
|
|
175
|
-
actor: msg.actor ?? 'websocket',
|
|
176
|
-
reason: msg.reason,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (!ok) {
|
|
180
|
-
ws.send(
|
|
181
|
-
JSON.stringify({
|
|
182
|
-
type: 'error',
|
|
183
|
-
message: `approval ${msg.approvalId} not found or already resolved`,
|
|
184
|
-
}),
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
close(ws) {
|
|
190
|
-
clients.delete(ws);
|
|
191
|
-
clientRates.delete(ws);
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
port: server.port!,
|
|
198
|
-
detach() {
|
|
199
|
-
unsubscribe();
|
|
200
|
-
clients.clear();
|
|
201
|
-
server.stop(true);
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ── Type guard ──
|
|
207
|
-
|
|
208
|
-
interface ResolveMessage {
|
|
209
|
-
type: 'resolve';
|
|
210
|
-
approvalId: string;
|
|
211
|
-
outcome: 'approved' | 'rejected';
|
|
212
|
-
actor?: string;
|
|
213
|
-
reason?: string;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isResolveMessage(v: unknown): v is ResolveMessage {
|
|
217
|
-
if (typeof v !== 'object' || v === null) return false;
|
|
218
|
-
const m = v as Record<string, unknown>;
|
|
219
|
-
return (
|
|
220
|
-
m['type'] === 'resolve' &&
|
|
221
|
-
typeof m['approvalId'] === 'string' &&
|
|
222
|
-
(m['outcome'] === 'approved' || m['outcome'] === 'rejected')
|
|
223
|
-
);
|
|
224
|
-
}
|
package/src/approval.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
import { nowISO } from './utils';
|
|
3
|
-
|
|
4
|
-
// Approval types (ApprovalRequest, ApprovalDecision, ApprovalOutcome,
|
|
5
|
-
// ApprovalEvent, ApprovalListener, ApprovalGateway) live in the shared
|
|
6
|
-
// @tagma/types package so trigger plugins can import them without
|
|
7
|
-
// depending on the engine's runtime implementation. This module keeps
|
|
8
|
-
// only the in-memory implementation. Internal SDK imports go through
|
|
9
|
-
// ./types (the engine-side re-export) for consistency with the rest of
|
|
10
|
-
// the SDK source.
|
|
11
|
-
import type {
|
|
12
|
-
ApprovalRequest,
|
|
13
|
-
ApprovalDecision,
|
|
14
|
-
ApprovalEvent,
|
|
15
|
-
ApprovalListener,
|
|
16
|
-
ApprovalGateway,
|
|
17
|
-
} from './types';
|
|
18
|
-
|
|
19
|
-
// Re-export for existing engine-side consumers that import from this file.
|
|
20
|
-
export type {
|
|
21
|
-
ApprovalRequest,
|
|
22
|
-
ApprovalDecision,
|
|
23
|
-
ApprovalOutcome,
|
|
24
|
-
ApprovalEvent,
|
|
25
|
-
ApprovalListener,
|
|
26
|
-
ApprovalGateway,
|
|
27
|
-
} from './types';
|
|
28
|
-
|
|
29
|
-
// ═══ Default In-Memory Implementation ═══
|
|
30
|
-
|
|
31
|
-
interface PendingEntry {
|
|
32
|
-
readonly request: ApprovalRequest;
|
|
33
|
-
readonly settle: (decision: ApprovalDecision) => void;
|
|
34
|
-
readonly timer: ReturnType<typeof setTimeout> | null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class InMemoryApprovalGateway implements ApprovalGateway {
|
|
38
|
-
private readonly pendingMap = new Map<string, PendingEntry>();
|
|
39
|
-
private readonly listeners = new Set<ApprovalListener>();
|
|
40
|
-
|
|
41
|
-
request(req: Omit<ApprovalRequest, 'id' | 'createdAt'>): Promise<ApprovalDecision> {
|
|
42
|
-
const full: ApprovalRequest = {
|
|
43
|
-
id: randomUUID(),
|
|
44
|
-
createdAt: nowISO(),
|
|
45
|
-
taskId: req.taskId,
|
|
46
|
-
trackId: req.trackId,
|
|
47
|
-
message: req.message,
|
|
48
|
-
timeoutMs: req.timeoutMs,
|
|
49
|
-
metadata: req.metadata,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return new Promise<ApprovalDecision>((resolvePromise) => {
|
|
53
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
-
if (full.timeoutMs > 0) {
|
|
55
|
-
timer = setTimeout(() => {
|
|
56
|
-
const entry = this.pendingMap.get(full.id);
|
|
57
|
-
if (!entry) return;
|
|
58
|
-
this.pendingMap.delete(full.id);
|
|
59
|
-
const decision: ApprovalDecision = {
|
|
60
|
-
approvalId: full.id,
|
|
61
|
-
outcome: 'timeout',
|
|
62
|
-
reason: `Approval timed out after ${full.timeoutMs}ms`,
|
|
63
|
-
decidedAt: nowISO(),
|
|
64
|
-
};
|
|
65
|
-
this.emit({ type: 'expired', request: full });
|
|
66
|
-
resolvePromise(decision);
|
|
67
|
-
}, full.timeoutMs);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
this.pendingMap.set(full.id, { request: full, settle: resolvePromise, timer });
|
|
71
|
-
this.emit({ type: 'requested', request: full });
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
resolve(
|
|
76
|
-
approvalId: string,
|
|
77
|
-
decision: Omit<ApprovalDecision, 'approvalId' | 'decidedAt'>,
|
|
78
|
-
): boolean {
|
|
79
|
-
const entry = this.pendingMap.get(approvalId);
|
|
80
|
-
if (!entry) return false;
|
|
81
|
-
this.pendingMap.delete(approvalId);
|
|
82
|
-
if (entry.timer) clearTimeout(entry.timer);
|
|
83
|
-
|
|
84
|
-
const full: ApprovalDecision = {
|
|
85
|
-
approvalId,
|
|
86
|
-
outcome: decision.outcome,
|
|
87
|
-
actor: decision.actor,
|
|
88
|
-
reason: decision.reason,
|
|
89
|
-
decidedAt: nowISO(),
|
|
90
|
-
};
|
|
91
|
-
this.emit({ type: 'resolved', request: entry.request, decision: full });
|
|
92
|
-
entry.settle(full);
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
pending(): readonly ApprovalRequest[] {
|
|
97
|
-
return Array.from(this.pendingMap.values()).map((e) => e.request);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
subscribe(listener: ApprovalListener): () => void {
|
|
101
|
-
this.listeners.add(listener);
|
|
102
|
-
return () => {
|
|
103
|
-
this.listeners.delete(listener);
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
abortAll(reason: string): void {
|
|
108
|
-
const entries = Array.from(this.pendingMap.entries());
|
|
109
|
-
this.pendingMap.clear();
|
|
110
|
-
for (const [id, entry] of entries) {
|
|
111
|
-
if (entry.timer) clearTimeout(entry.timer);
|
|
112
|
-
this.emit({ type: 'aborted', request: entry.request, reason });
|
|
113
|
-
entry.settle({
|
|
114
|
-
approvalId: id,
|
|
115
|
-
outcome: 'aborted',
|
|
116
|
-
reason,
|
|
117
|
-
decidedAt: nowISO(),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private emit(event: ApprovalEvent): void {
|
|
123
|
-
for (const listener of this.listeners) {
|
|
124
|
-
try {
|
|
125
|
-
listener(event);
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.error('[approval gateway] listener error:', err);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
package/src/bootstrap.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { PluginRegistry } from './registry';
|
|
2
|
-
import type { TagmaPlugin } from './types';
|
|
3
|
-
|
|
4
|
-
// Built-in Drivers
|
|
5
|
-
// Only opencode is built in. Other drivers (codex, claude-code) ship as
|
|
6
|
-
// workspace plugins under packages/ and must be declared in pipeline.yaml
|
|
7
|
-
// via the `plugins` field, e.g.:
|
|
8
|
-
// plugins: ["@tagma/driver-codex", "@tagma/driver-claude-code"]
|
|
9
|
-
import { OpenCodeDriver } from './drivers/opencode';
|
|
10
|
-
|
|
11
|
-
// Built-in Triggers
|
|
12
|
-
import { FileTrigger } from './triggers/file';
|
|
13
|
-
import { ManualTrigger } from './triggers/manual';
|
|
14
|
-
|
|
15
|
-
// Built-in Completions
|
|
16
|
-
import { ExitCodeCompletion } from './completions/exit-code';
|
|
17
|
-
import { FileExistsCompletion } from './completions/file-exists';
|
|
18
|
-
import { OutputCheckCompletion } from './completions/output-check';
|
|
19
|
-
|
|
20
|
-
// Built-in Middleware
|
|
21
|
-
import { StaticContextMiddleware } from './middlewares/static-context';
|
|
22
|
-
|
|
23
|
-
export const BuiltinTagmaPlugin = {
|
|
24
|
-
name: '@tagma/sdk/builtins',
|
|
25
|
-
capabilities: {
|
|
26
|
-
drivers: {
|
|
27
|
-
opencode: OpenCodeDriver,
|
|
28
|
-
},
|
|
29
|
-
triggers: {
|
|
30
|
-
file: FileTrigger,
|
|
31
|
-
manual: ManualTrigger,
|
|
32
|
-
},
|
|
33
|
-
completions: {
|
|
34
|
-
exit_code: ExitCodeCompletion,
|
|
35
|
-
file_exists: FileExistsCompletion,
|
|
36
|
-
output_check: OutputCheckCompletion,
|
|
37
|
-
},
|
|
38
|
-
middlewares: {
|
|
39
|
-
static_context: StaticContextMiddleware,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
} satisfies TagmaPlugin;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Register every built-in plugin into `target`. Hosts instantiate one
|
|
46
|
-
* PluginRegistry per workspace or SDK instance and call this once per
|
|
47
|
-
* instance so each workspace sees the same built-ins without sharing
|
|
48
|
-
* registration state.
|
|
49
|
-
*
|
|
50
|
-
* Built-in handlers are stateless module singletons — registering the same
|
|
51
|
-
* handler object into N registries is cheap and safe; no cloning is needed.
|
|
52
|
-
*/
|
|
53
|
-
export function bootstrapBuiltins(target: PluginRegistry): void {
|
|
54
|
-
target.registerTagmaPlugin(BuiltinTagmaPlugin);
|
|
55
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
2
|
-
|
|
3
|
-
export const ExitCodeCompletion: CompletionPlugin = {
|
|
4
|
-
name: 'exit_code',
|
|
5
|
-
schema: {
|
|
6
|
-
description: 'Mark the task successful when the exit code matches.',
|
|
7
|
-
fields: {
|
|
8
|
-
expect: {
|
|
9
|
-
type: 'number-or-list',
|
|
10
|
-
default: 0,
|
|
11
|
-
description: 'Expected exit code, or list of acceptable codes (e.g. 0 or [0, 2]).',
|
|
12
|
-
placeholder: '0',
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
async check(
|
|
18
|
-
config: Record<string, unknown>,
|
|
19
|
-
result: TaskResult,
|
|
20
|
-
_ctx: CompletionContext,
|
|
21
|
-
): Promise<boolean> {
|
|
22
|
-
const expected = config.expect ?? 0;
|
|
23
|
-
|
|
24
|
-
if (typeof expected === 'number') {
|
|
25
|
-
return result.exitCode === expected;
|
|
26
|
-
}
|
|
27
|
-
if (Array.isArray(expected) && expected.every((v) => typeof v === 'number')) {
|
|
28
|
-
return expected.includes(result.exitCode);
|
|
29
|
-
}
|
|
30
|
-
throw new Error(
|
|
31
|
-
`exit_code completion: "expect" must be a number or number[], got ${typeof expected}`,
|
|
32
|
-
);
|
|
33
|
-
},
|
|
34
|
-
};
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
2
|
-
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
3
|
-
import { validatePath } from '../utils';
|
|
4
|
-
|
|
5
|
-
type Kind = 'file' | 'dir' | 'any';
|
|
6
|
-
|
|
7
|
-
export const FileExistsCompletion: CompletionPlugin = {
|
|
8
|
-
name: 'file_exists',
|
|
9
|
-
schema: {
|
|
10
|
-
description: 'Mark the task successful when a target file or directory exists.',
|
|
11
|
-
fields: {
|
|
12
|
-
path: {
|
|
13
|
-
type: 'path',
|
|
14
|
-
required: true,
|
|
15
|
-
description: 'Path to check (relative to workDir or absolute).',
|
|
16
|
-
},
|
|
17
|
-
kind: {
|
|
18
|
-
type: 'enum',
|
|
19
|
-
enum: ['file', 'dir', 'any'],
|
|
20
|
-
default: 'any',
|
|
21
|
-
description: 'Restrict to a file, directory, or accept either.',
|
|
22
|
-
},
|
|
23
|
-
min_size: {
|
|
24
|
-
type: 'number',
|
|
25
|
-
min: 0,
|
|
26
|
-
description: 'Optional minimum size in bytes (files only).',
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
async check(
|
|
32
|
-
config: Record<string, unknown>,
|
|
33
|
-
_result: TaskResult,
|
|
34
|
-
ctx: CompletionContext,
|
|
35
|
-
): Promise<boolean> {
|
|
36
|
-
const filePath = config.path as string;
|
|
37
|
-
if (!filePath) throw new Error('file_exists completion: "path" is required');
|
|
38
|
-
|
|
39
|
-
const safePath = validatePath(filePath, ctx.workDir);
|
|
40
|
-
|
|
41
|
-
const kind = (config.kind as Kind | undefined) ?? 'any';
|
|
42
|
-
if (kind !== 'file' && kind !== 'dir' && kind !== 'any') {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`file_exists completion: "kind" must be "file" | "dir" | "any", got "${kind}"`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const minSize = config.min_size;
|
|
49
|
-
if (minSize != null && (typeof minSize !== 'number' || minSize < 0)) {
|
|
50
|
-
throw new Error(`file_exists completion: "min_size" must be a non-negative number`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const st = await stat(safePath);
|
|
55
|
-
if (kind === 'file' && !st.isFile()) return false;
|
|
56
|
-
if (kind === 'dir' && !st.isDirectory()) return false;
|
|
57
|
-
if (typeof minSize === 'number' && st.isFile() && st.size < minSize) return false;
|
|
58
|
-
return true;
|
|
59
|
-
} catch (err: unknown) {
|
|
60
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
61
|
-
if (code === 'ENOENT' || code === 'ENOTDIR') return false;
|
|
62
|
-
// Permission / IO errors should surface, not silently mean "missing"
|
|
63
|
-
throw err;
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test';
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { OutputCheckCompletion } from './output-check';
|
|
6
|
-
import type { TaskResult } from '../types';
|
|
7
|
-
|
|
8
|
-
function taskResult(stdout = 'payload'): TaskResult {
|
|
9
|
-
return {
|
|
10
|
-
exitCode: 0,
|
|
11
|
-
stdout,
|
|
12
|
-
stderr: '',
|
|
13
|
-
stdoutPath: null,
|
|
14
|
-
stderrPath: null,
|
|
15
|
-
stdoutBytes: stdout.length,
|
|
16
|
-
stderrBytes: 0,
|
|
17
|
-
durationMs: 0,
|
|
18
|
-
sessionId: null,
|
|
19
|
-
normalizedOutput: null,
|
|
20
|
-
failureKind: null,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test(
|
|
25
|
-
'output_check drains verbose check stdout so the check process can exit',
|
|
26
|
-
async () => {
|
|
27
|
-
const dir = mkdtempSync(join(tmpdir(), 'tagma-output-check-'));
|
|
28
|
-
try {
|
|
29
|
-
const script = join(dir, 'verbose-check.js');
|
|
30
|
-
writeFileSync(
|
|
31
|
-
script,
|
|
32
|
-
'process.stdout.write("x".repeat(16 * 1024 * 1024));\n',
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const passed = await OutputCheckCompletion.check(
|
|
36
|
-
{
|
|
37
|
-
check: `node "${script}"`,
|
|
38
|
-
timeout: '1s',
|
|
39
|
-
},
|
|
40
|
-
taskResult(),
|
|
41
|
-
{ workDir: dir, signal: new AbortController().signal },
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
expect(passed).toBe(true);
|
|
45
|
-
} finally {
|
|
46
|
-
rmSync(dir, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
5_000,
|
|
50
|
-
);
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import type { CompletionPlugin, CompletionContext, TaskResult } from '../types';
|
|
2
|
-
import { shellArgs, parseDuration } from '../utils';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
-
|
|
6
|
-
export const OutputCheckCompletion: CompletionPlugin = {
|
|
7
|
-
name: 'output_check',
|
|
8
|
-
schema: {
|
|
9
|
-
description:
|
|
10
|
-
'Pipe the task output into a shell command; mark success when that command exits 0. For AI driver tasks the driver-normalized text is piped (not the raw NDJSON); command tasks see their raw stdout.',
|
|
11
|
-
fields: {
|
|
12
|
-
check: {
|
|
13
|
-
type: 'string',
|
|
14
|
-
required: true,
|
|
15
|
-
description:
|
|
16
|
-
'Shell command to run. The task output is piped to its stdin — normalizedOutput when the driver provides one, otherwise raw stdout.',
|
|
17
|
-
placeholder: "grep -q 'PASS'",
|
|
18
|
-
},
|
|
19
|
-
timeout: {
|
|
20
|
-
type: 'duration',
|
|
21
|
-
default: '30s',
|
|
22
|
-
description: 'Maximum time to wait for the check command.',
|
|
23
|
-
placeholder: '30s',
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
async check(
|
|
29
|
-
config: Record<string, unknown>,
|
|
30
|
-
result: TaskResult,
|
|
31
|
-
ctx: CompletionContext,
|
|
32
|
-
): Promise<boolean> {
|
|
33
|
-
const checkCmd = config.check as string;
|
|
34
|
-
if (!checkCmd) throw new Error('output_check completion: "check" is required');
|
|
35
|
-
|
|
36
|
-
const timeoutMs =
|
|
37
|
-
config.timeout != null ? parseDuration(String(config.timeout)) : DEFAULT_TIMEOUT_MS;
|
|
38
|
-
|
|
39
|
-
const controller = new AbortController();
|
|
40
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
41
|
-
|
|
42
|
-
// Wire pipeline abort signal into the check process so external abort
|
|
43
|
-
// terminates the child instead of leaving it running undetected.
|
|
44
|
-
const onAbort = () => controller.abort();
|
|
45
|
-
if (ctx.signal) {
|
|
46
|
-
if (ctx.signal.aborted) {
|
|
47
|
-
controller.abort();
|
|
48
|
-
} else {
|
|
49
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const proc = Bun.spawn(shellArgs(checkCmd) as string[], {
|
|
54
|
-
cwd: ctx.workDir,
|
|
55
|
-
stdin: 'pipe',
|
|
56
|
-
stdout: 'pipe',
|
|
57
|
-
stderr: 'pipe',
|
|
58
|
-
signal: controller.signal,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
if (proc.stdin) {
|
|
63
|
-
try {
|
|
64
|
-
// Prefer driver-normalized text (e.g. concatenated message text for
|
|
65
|
-
// AI drivers that emit NDJSON). Falling back to raw stdout keeps
|
|
66
|
-
// command tasks and drivers without parseResult working.
|
|
67
|
-
const payload = result.normalizedOutput ?? result.stdout;
|
|
68
|
-
proc.stdin.write(payload);
|
|
69
|
-
proc.stdin.end(); // no await — consistent with runner.ts; proc.exited handles sync
|
|
70
|
-
} catch (err: unknown) {
|
|
71
|
-
// EPIPE is expected when the check process exits before reading all of stdin
|
|
72
|
-
// (e.g. `grep -q` exits on first match). Anything else is a real failure.
|
|
73
|
-
const code = (err as NodeJS.ErrnoException)?.code;
|
|
74
|
-
if (code !== 'EPIPE') throw err;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Consume stderr concurrently with waiting for exit to prevent pipe-buffer
|
|
79
|
-
// deadlock when check script emits more than ~64 KB of stderr output.
|
|
80
|
-
const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
81
|
-
|
|
82
|
-
if (exitCode !== 0 && stderr.trim()) {
|
|
83
|
-
console.warn(`[output_check] "${checkCmd}" exit=${exitCode}: ${stderr.trim()}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return exitCode === 0;
|
|
87
|
-
} finally {
|
|
88
|
-
clearTimeout(timer);
|
|
89
|
-
if (ctx.signal) ctx.signal.removeEventListener('abort', onAbort);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
};
|