@fusionkit/plane 0.1.0

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 (52) hide show
  1. package/dist/auth.d.ts +18 -0
  2. package/dist/auth.js +46 -0
  3. package/dist/claim-token-service.d.ts +23 -0
  4. package/dist/claim-token-service.js +54 -0
  5. package/dist/contract-service.d.ts +14 -0
  6. package/dist/contract-service.js +39 -0
  7. package/dist/domain-errors.d.ts +13 -0
  8. package/dist/domain-errors.js +31 -0
  9. package/dist/idp.d.ts +26 -0
  10. package/dist/idp.js +24 -0
  11. package/dist/index.d.ts +35 -0
  12. package/dist/index.js +21 -0
  13. package/dist/keys.d.ts +60 -0
  14. package/dist/keys.js +132 -0
  15. package/dist/logging.d.ts +21 -0
  16. package/dist/logging.js +42 -0
  17. package/dist/plane.d.ts +167 -0
  18. package/dist/plane.js +606 -0
  19. package/dist/policy.d.ts +23 -0
  20. package/dist/policy.js +92 -0
  21. package/dist/ratelimit.d.ts +40 -0
  22. package/dist/ratelimit.js +94 -0
  23. package/dist/receipt-service.d.ts +16 -0
  24. package/dist/receipt-service.js +17 -0
  25. package/dist/retention.d.ts +33 -0
  26. package/dist/retention.js +123 -0
  27. package/dist/run-lifecycle.d.ts +2 -0
  28. package/dist/run-lifecycle.js +19 -0
  29. package/dist/secrets.d.ts +25 -0
  30. package/dist/secrets.js +73 -0
  31. package/dist/server.d.ts +38 -0
  32. package/dist/server.js +418 -0
  33. package/dist/sqlite-store.d.ts +53 -0
  34. package/dist/sqlite-store.js +401 -0
  35. package/dist/store.d.ts +107 -0
  36. package/dist/store.js +9 -0
  37. package/dist/test/api.test.d.ts +1 -0
  38. package/dist/test/api.test.js +179 -0
  39. package/dist/test/hardening.test.d.ts +1 -0
  40. package/dist/test/hardening.test.js +259 -0
  41. package/dist/test/policy.test.d.ts +1 -0
  42. package/dist/test/policy.test.js +78 -0
  43. package/dist/test/server-hardening.test.d.ts +1 -0
  44. package/dist/test/server-hardening.test.js +192 -0
  45. package/dist/test/ui-parity.test.d.ts +1 -0
  46. package/dist/test/ui-parity.test.js +28 -0
  47. package/dist/validation.d.ts +326 -0
  48. package/dist/validation.js +178 -0
  49. package/package.json +34 -0
  50. package/ui/app.css +276 -0
  51. package/ui/app.js +483 -0
  52. package/ui/index.html +65 -0
@@ -0,0 +1,178 @@
1
+ import { z } from "zod";
2
+ import { ACTOR_KINDS, AGENT_KINDS, CHECKPOINT_TIERS, DISCLOSURE_MODES, HEX_HASH_PATTERN, parseHostAllowlistEntry, parsePoolName, parseSecretName, parseWorkspaceManifestPath, PROTOCOL_VERSIONS, SESSION_ISOLATIONS } from "@fusionkit/protocol";
3
+ import { PRINCIPAL_ROLES } from "./store.js";
4
+ /**
5
+ * Boundary validation for request bodies. Signed objects (chained events,
6
+ * receipts) are validated cryptographically by the plane, not re-described
7
+ * here; these schemas guard the unsigned inputs (run requests, actors,
8
+ * tokens) that nothing else checks. Parsing returns structured errors that
9
+ * the server turns into 400s.
10
+ */
11
+ // The max lengths below are generous DoS guards (a 1M-char prompt, 100k
12
+ // untracked files), not policy limits — policy is enforced separately by the
13
+ // plane's policy engine. They exist to reject obviously abusive payloads at
14
+ // the boundary before any work is done.
15
+ const actorSchema = z.object({
16
+ kind: z.enum(ACTOR_KINDS),
17
+ id: z.string().min(1).max(256)
18
+ });
19
+ const manifestFileSchema = z.object({
20
+ path: z.string().min(1).max(4096).transform(parseWorkspaceManifestPath),
21
+ hash: z.string().regex(HEX_HASH_PATTERN),
22
+ bytes: z.number().int().nonnegative()
23
+ });
24
+ const workspaceSchema = z.object({
25
+ version: z.literal(PROTOCOL_VERSIONS.manifest),
26
+ baseRef: z.string().min(1).max(256),
27
+ bundleHash: z.string().regex(HEX_HASH_PATTERN),
28
+ dirtyDiffHash: z.string().regex(HEX_HASH_PATTERN).optional(),
29
+ untrackedFiles: z.array(manifestFileSchema).max(100000),
30
+ deniedPatterns: z.array(z.string().max(512)).max(10000),
31
+ deniedPaths: z
32
+ .array(z.string().max(4096).transform(parseWorkspaceManifestPath))
33
+ .max(100000)
34
+ });
35
+ const networkSchema = z.object({
36
+ defaultDeny: z.boolean(),
37
+ allowHosts: z.array(z.string().min(1).max(253).transform(parseHostAllowlistEntry)).max(1000)
38
+ });
39
+ const budgetSchema = z.object({
40
+ maxSpendUsd: z.number().nonnegative().optional(),
41
+ maxDurationMin: z.number().positive().optional()
42
+ });
43
+ const continuationSchema = z.object({
44
+ envelopeHash: z.string().regex(HEX_HASH_PATTERN),
45
+ checkpointId: z.string().min(1).max(256),
46
+ tier: z.enum(CHECKPOINT_TIERS)
47
+ });
48
+ const executionEnvSchema = z
49
+ .object({
50
+ inherit: z.array(z.string().min(1).max(128)).max(256).optional(),
51
+ secrets: z
52
+ .array(z.object({
53
+ env: z.string().min(1).max(256).transform(parseSecretName),
54
+ secretName: z.string().min(1).max(256).transform(parseSecretName)
55
+ }))
56
+ .max(256)
57
+ .optional(),
58
+ vars: z.record(z.string().min(1).max(256), z.string().max(100000)).optional(),
59
+ egressProxy: z.boolean().optional()
60
+ })
61
+ .strict();
62
+ const executionLogSchema = z
63
+ .object({
64
+ stdout: z.literal("capture"),
65
+ stderr: z.enum(["merge", "capture"]),
66
+ maxBytes: z.number().int().positive().optional()
67
+ })
68
+ .strict();
69
+ const executionSchema = z.discriminatedUnion("kind", [
70
+ z
71
+ .object({
72
+ kind: z.literal("shell"),
73
+ script: z.string().min(1).max(1_000_000),
74
+ shell: z.enum(["sh", "bash"]).optional(),
75
+ cwd: z.string().min(1).max(4096).transform(parseWorkspaceManifestPath).optional(),
76
+ timeoutMs: z.number().int().positive().optional(),
77
+ env: executionEnvSchema.optional(),
78
+ log: executionLogSchema.optional()
79
+ })
80
+ .strict(),
81
+ z
82
+ .object({
83
+ kind: z.literal("argv"),
84
+ command: z.string().min(1).max(4096),
85
+ args: z.array(z.string().max(100000)).max(10000),
86
+ cwd: z.string().min(1).max(4096).transform(parseWorkspaceManifestPath).optional(),
87
+ timeoutMs: z.number().int().positive().optional(),
88
+ env: executionEnvSchema.optional(),
89
+ log: executionLogSchema.optional()
90
+ })
91
+ .strict(),
92
+ z
93
+ .object({
94
+ kind: z.literal("agent"),
95
+ agent: z
96
+ .object({
97
+ kind: z.enum(AGENT_KINDS),
98
+ version: z.string().max(128).optional()
99
+ })
100
+ .strict(),
101
+ prompt: z.string().min(1).max(1_000_000),
102
+ timeoutMs: z.number().int().positive().optional(),
103
+ env: executionEnvSchema.optional(),
104
+ log: executionLogSchema.optional()
105
+ })
106
+ .strict()
107
+ ]);
108
+ export const runRequestSchema = z.object({
109
+ requestedBy: actorSchema,
110
+ // agentKind is validated structurally; whether the kind is *permitted* is
111
+ // the policy engine's decision at contract time, not the schema's.
112
+ agentKind: z.string().min(1).max(64),
113
+ agentVersion: z.string().max(128).optional(),
114
+ prompt: z.string().min(1).max(1_000_000),
115
+ pool: z.string().min(1).max(128).transform(parsePoolName),
116
+ secretNames: z.array(z.string().min(1).max(256).transform(parseSecretName)).max(256),
117
+ workspace: workspaceSchema,
118
+ network: networkSchema,
119
+ budget: budgetSchema,
120
+ disclosure: z.enum(DISCLOSURE_MODES),
121
+ execution: executionSchema.optional(),
122
+ isolation: z.enum(SESSION_ISOLATIONS).optional(),
123
+ continuation: continuationSchema.optional()
124
+ });
125
+ export const createRunBodySchema = z.object({
126
+ dryRun: z.boolean().optional(),
127
+ request: runRequestSchema
128
+ });
129
+ export const enrollBodySchema = z.object({
130
+ enrollToken: z.string().min(1).max(4096),
131
+ publicKeyPem: z.string().min(1).max(8192),
132
+ pool: z.string().min(1).max(128).transform(parsePoolName)
133
+ });
134
+ export const claimBodySchema = z.object({
135
+ runnerToken: z.string().min(1).max(4096),
136
+ pool: z.string().min(1).max(128).transform(parsePoolName)
137
+ });
138
+ export const approveBodySchema = z.object({
139
+ actor: actorSchema.optional(),
140
+ /** Optional IdP-issued JWT; when present, the approval is bound to its subject. */
141
+ idpToken: z.string().min(1).max(8192).optional()
142
+ });
143
+ export const cancelBodySchema = z.object({
144
+ actor: actorSchema.optional()
145
+ });
146
+ // Events and receipts are signed, hash-chained objects. Their integrity is
147
+ // verified cryptographically inside the plane (chain verification + runner
148
+ // signature), which is a stronger gate than any structural schema. The
149
+ // schema here only bounds the envelope shape/size; the crypto check is
150
+ // authoritative, so re-describing the full object as zod would be redundant.
151
+ export const eventsBodySchema = z.object({
152
+ claimToken: z.string().min(1).max(8192),
153
+ events: z.array(z.unknown()).max(100000)
154
+ });
155
+ export const completeBodySchema = z.object({
156
+ claimToken: z.string().min(1).max(8192),
157
+ receipt: z.unknown()
158
+ });
159
+ export const issuePrincipalBodySchema = z.object({
160
+ name: z.string().min(1).max(128),
161
+ role: z.enum(PRINCIPAL_ROLES)
162
+ });
163
+ export class ValidationError extends Error {
164
+ issues;
165
+ constructor(issues) {
166
+ super(`invalid request: ${issues.join("; ")}`);
167
+ this.name = "ValidationError";
168
+ this.issues = issues;
169
+ }
170
+ }
171
+ export function parseBody(schema, raw) {
172
+ const result = schema.safeParse(raw);
173
+ if (!result.success) {
174
+ const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
175
+ throw new ValidationError(issues);
176
+ }
177
+ return result.data;
178
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@fusionkit/plane",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/velum-labs/handoffkit.git",
8
+ "directory": "packages/plane"
9
+ },
10
+ "description": "Warrant control plane: contracts, policy, approvals, receipt countersignature, secret broker, audit export, and the control panel UI.",
11
+ "license": "UNLICENSED",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "ui"
22
+ ],
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org",
25
+ "access": "public",
26
+ "provenance": true
27
+ },
28
+ "dependencies": {
29
+ "jose": "6.2.3",
30
+ "pino": "10.3.1",
31
+ "zod": "4.4.3",
32
+ "@fusionkit/protocol": "0.1.0"
33
+ }
34
+ }
package/ui/app.css ADDED
@@ -0,0 +1,276 @@
1
+ :root {
2
+ --bg: #0b0e14;
3
+ --bg-raised: #11151f;
4
+ --bg-hover: #161b28;
5
+ --border: #232a3a;
6
+ --text: #d7dce6;
7
+ --text-dim: #8b94a7;
8
+ --accent: #7aa2f7;
9
+ --accent-dim: #2a3756;
10
+ --good: #34d399;
11
+ --bad: #f87171;
12
+ --warn: #fbbf24;
13
+ --info: #60a5fa;
14
+ --mono: "SF Mono", ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
15
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
16
+ }
17
+
18
+ * { box-sizing: border-box; }
19
+
20
+ [hidden] { display: none !important; }
21
+
22
+ body {
23
+ margin: 0;
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ font-family: var(--sans);
27
+ font-size: 14px;
28
+ line-height: 1.5;
29
+ min-height: 100vh;
30
+ display: flex;
31
+ flex-direction: column;
32
+ }
33
+
34
+ main { flex: 1; width: 100%; max-width: 1100px; margin: 0 auto; padding: 24px 20px 48px; }
35
+
36
+ code, .mono { font-family: var(--mono); font-size: 12.5px; }
37
+
38
+ /* ---------- chrome ---------- */
39
+
40
+ .topbar {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 28px;
44
+ padding: 14px 20px;
45
+ border-bottom: 1px solid var(--border);
46
+ background: var(--bg-raised);
47
+ position: sticky;
48
+ top: 0;
49
+ z-index: 10;
50
+ }
51
+
52
+ .brand { display: flex; align-items: baseline; gap: 8px; }
53
+ .brand-mark { color: var(--accent); font-size: 18px; }
54
+ .brand-name { font-weight: 700; letter-spacing: 0.04em; font-size: 16px; }
55
+ .brand-sub { color: var(--text-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.12em; }
56
+
57
+ .nav { display: flex; gap: 4px; }
58
+ .nav a {
59
+ color: var(--text-dim);
60
+ text-decoration: none;
61
+ padding: 6px 12px;
62
+ border-radius: 6px;
63
+ font-weight: 500;
64
+ }
65
+ .nav a:hover { color: var(--text); background: var(--bg-hover); }
66
+ .nav a.active { color: var(--accent); background: var(--accent-dim); }
67
+
68
+ .top-actions { margin-left: auto; display: flex; gap: 8px; }
69
+
70
+ .footer {
71
+ display: flex;
72
+ justify-content: space-between;
73
+ gap: 16px;
74
+ padding: 14px 20px;
75
+ border-top: 1px solid var(--border);
76
+ color: var(--text-dim);
77
+ font-size: 12px;
78
+ }
79
+
80
+ /* ---------- buttons ---------- */
81
+
82
+ .btn {
83
+ font: inherit;
84
+ border: 1px solid var(--border);
85
+ background: var(--bg-raised);
86
+ color: var(--text);
87
+ border-radius: 6px;
88
+ padding: 6px 14px;
89
+ cursor: pointer;
90
+ }
91
+ .btn:hover { background: var(--bg-hover); }
92
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: #0b0e14; font-weight: 600; }
93
+ .btn-primary:hover { filter: brightness(1.1); background: var(--accent); }
94
+ .btn-ghost { background: transparent; color: var(--text-dim); }
95
+ .btn-ghost:hover { color: var(--text); }
96
+ .btn-danger { color: var(--bad); border-color: var(--bad); background: transparent; }
97
+ .btn-danger:hover { background: rgba(248, 113, 113, 0.1); }
98
+ .btn-good { color: var(--good); border-color: var(--good); background: transparent; }
99
+ .btn-good:hover { background: rgba(52, 211, 153, 0.1); }
100
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
101
+
102
+ /* ---------- login ---------- */
103
+
104
+ .login { display: flex; justify-content: center; padding-top: 8vh; }
105
+ .login-card {
106
+ width: 460px;
107
+ background: var(--bg-raised);
108
+ border: 1px solid var(--border);
109
+ border-radius: 12px;
110
+ padding: 32px;
111
+ }
112
+ .login-card h1 { margin: 0 0 8px; font-size: 20px; }
113
+ .login-card p { color: var(--text-dim); margin: 0 0 20px; }
114
+ .login-card form { display: flex; gap: 8px; }
115
+ .login-card input {
116
+ flex: 1;
117
+ font-family: var(--mono);
118
+ font-size: 13px;
119
+ background: var(--bg);
120
+ border: 1px solid var(--border);
121
+ border-radius: 6px;
122
+ color: var(--text);
123
+ padding: 8px 10px;
124
+ }
125
+ .login-card input:focus { outline: none; border-color: var(--accent); }
126
+ .login-error { color: var(--bad); margin-top: 12px; }
127
+
128
+ /* ---------- shared view bits ---------- */
129
+
130
+ .view-head {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 12px;
134
+ margin: 4px 0 18px;
135
+ }
136
+ .view-head h1 { font-size: 18px; margin: 0; }
137
+ .view-head .count { color: var(--text-dim); }
138
+ .view-head .spacer { flex: 1; }
139
+ .muted { color: var(--text-dim); }
140
+ .empty {
141
+ border: 1px dashed var(--border);
142
+ border-radius: 10px;
143
+ padding: 36px;
144
+ text-align: center;
145
+ color: var(--text-dim);
146
+ }
147
+ .empty code { color: var(--text); }
148
+
149
+ table { width: 100%; border-collapse: collapse; }
150
+ th {
151
+ text-align: left;
152
+ color: var(--text-dim);
153
+ font-size: 11px;
154
+ text-transform: uppercase;
155
+ letter-spacing: 0.08em;
156
+ font-weight: 600;
157
+ padding: 8px 12px;
158
+ border-bottom: 1px solid var(--border);
159
+ }
160
+ td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
161
+ tr.row-link { cursor: pointer; }
162
+ tr.row-link:hover td { background: var(--bg-hover); }
163
+
164
+ .chip {
165
+ display: inline-block;
166
+ font-family: var(--mono);
167
+ font-size: 11px;
168
+ padding: 2px 8px;
169
+ border-radius: 999px;
170
+ border: 1px solid var(--border);
171
+ color: var(--text-dim);
172
+ white-space: nowrap;
173
+ }
174
+ .chip.completed { color: var(--good); border-color: var(--good); }
175
+ .chip.failed, .chip.cancelled { color: var(--bad); border-color: var(--bad); }
176
+ .chip.running, .chip.claimed, .chip.provisioning { color: var(--info); border-color: var(--info); }
177
+ .chip.awaiting_approval { color: var(--warn); border-color: var(--warn); }
178
+ .chip.created { color: var(--text); border-color: var(--text-dim); }
179
+ .chip.continuation { color: var(--accent); border-color: var(--accent); }
180
+
181
+ .hash { font-family: var(--mono); font-size: 12px; color: var(--text-dim); }
182
+
183
+ /* ---------- run detail ---------- */
184
+
185
+ .detail-grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 16px; }
186
+ @media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }
187
+
188
+ .card {
189
+ background: var(--bg-raised);
190
+ border: 1px solid var(--border);
191
+ border-radius: 10px;
192
+ padding: 18px;
193
+ margin-bottom: 16px;
194
+ }
195
+ .card h2 {
196
+ margin: 0 0 12px;
197
+ font-size: 12px;
198
+ color: var(--text-dim);
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.1em;
201
+ }
202
+ .card dl { margin: 0; display: grid; grid-template-columns: 150px 1fr; row-gap: 8px; }
203
+ .card dt { color: var(--text-dim); }
204
+ .card dd { margin: 0; overflow-wrap: anywhere; }
205
+
206
+ .actions { display: flex; gap: 8px; flex-wrap: wrap; }
207
+
208
+ .five-q .q { margin-bottom: 14px; }
209
+ .five-q .q:last-child { margin-bottom: 0; }
210
+ .five-q .q h3 { margin: 0 0 4px; font-size: 13px; color: var(--accent); }
211
+ .five-q .q div { color: var(--text); overflow-wrap: anywhere; }
212
+ .five-q .q .sub { color: var(--text-dim); font-size: 13px; }
213
+
214
+ .timeline { list-style: none; margin: 0; padding: 0; }
215
+ .timeline li {
216
+ display: flex;
217
+ gap: 10px;
218
+ padding: 7px 0;
219
+ border-bottom: 1px solid var(--border);
220
+ align-items: baseline;
221
+ }
222
+ .timeline li:last-child { border-bottom: none; }
223
+ .timeline .seq { font-family: var(--mono); font-size: 11px; color: var(--text-dim); width: 28px; flex-shrink: 0; }
224
+ .timeline .etype {
225
+ font-family: var(--mono);
226
+ font-size: 12px;
227
+ width: 190px;
228
+ flex-shrink: 0;
229
+ }
230
+ .timeline .edetail { color: var(--text-dim); font-size: 12.5px; overflow-wrap: anywhere; }
231
+ .timeline .ok { color: var(--good); }
232
+ .timeline .err { color: var(--bad); }
233
+ .timeline .warn { color: var(--warn); }
234
+ .timeline .info { color: var(--info); }
235
+ .timeline .plain { color: var(--text); }
236
+
237
+ .banner {
238
+ border: 1px solid var(--warn);
239
+ background: rgba(251, 191, 36, 0.08);
240
+ color: var(--warn);
241
+ border-radius: 8px;
242
+ padding: 10px 14px;
243
+ margin-bottom: 16px;
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 12px;
247
+ }
248
+ .banner .spacer { flex: 1; }
249
+
250
+ .verify-ok { color: var(--good); }
251
+ .verify-info { color: var(--text-dim); font-size: 12.5px; }
252
+
253
+ pre.policy {
254
+ background: var(--bg-raised);
255
+ border: 1px solid var(--border);
256
+ border-radius: 10px;
257
+ padding: 18px;
258
+ overflow-x: auto;
259
+ font-family: var(--mono);
260
+ font-size: 12.5px;
261
+ line-height: 1.6;
262
+ }
263
+
264
+ .toast {
265
+ position: fixed;
266
+ bottom: 24px;
267
+ right: 24px;
268
+ background: var(--bg-raised);
269
+ border: 1px solid var(--border);
270
+ border-left: 3px solid var(--accent);
271
+ border-radius: 8px;
272
+ padding: 10px 16px;
273
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
274
+ z-index: 100;
275
+ }
276
+ .toast.error { border-left-color: var(--bad); }