@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.
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +46 -0
- package/dist/claim-token-service.d.ts +23 -0
- package/dist/claim-token-service.js +54 -0
- package/dist/contract-service.d.ts +14 -0
- package/dist/contract-service.js +39 -0
- package/dist/domain-errors.d.ts +13 -0
- package/dist/domain-errors.js +31 -0
- package/dist/idp.d.ts +26 -0
- package/dist/idp.js +24 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +21 -0
- package/dist/keys.d.ts +60 -0
- package/dist/keys.js +132 -0
- package/dist/logging.d.ts +21 -0
- package/dist/logging.js +42 -0
- package/dist/plane.d.ts +167 -0
- package/dist/plane.js +606 -0
- package/dist/policy.d.ts +23 -0
- package/dist/policy.js +92 -0
- package/dist/ratelimit.d.ts +40 -0
- package/dist/ratelimit.js +94 -0
- package/dist/receipt-service.d.ts +16 -0
- package/dist/receipt-service.js +17 -0
- package/dist/retention.d.ts +33 -0
- package/dist/retention.js +123 -0
- package/dist/run-lifecycle.d.ts +2 -0
- package/dist/run-lifecycle.js +19 -0
- package/dist/secrets.d.ts +25 -0
- package/dist/secrets.js +73 -0
- package/dist/server.d.ts +38 -0
- package/dist/server.js +418 -0
- package/dist/sqlite-store.d.ts +53 -0
- package/dist/sqlite-store.js +401 -0
- package/dist/store.d.ts +107 -0
- package/dist/store.js +9 -0
- package/dist/test/api.test.d.ts +1 -0
- package/dist/test/api.test.js +179 -0
- package/dist/test/hardening.test.d.ts +1 -0
- package/dist/test/hardening.test.js +259 -0
- package/dist/test/policy.test.d.ts +1 -0
- package/dist/test/policy.test.js +78 -0
- package/dist/test/server-hardening.test.d.ts +1 -0
- package/dist/test/server-hardening.test.js +192 -0
- package/dist/test/ui-parity.test.d.ts +1 -0
- package/dist/test/ui-parity.test.js +28 -0
- package/dist/validation.d.ts +326 -0
- package/dist/validation.js +178 -0
- package/package.json +34 -0
- package/ui/app.css +276 -0
- package/ui/app.js +483 -0
- 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); }
|