@adtl/gateway 0.5.1 → 0.7.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 (102) hide show
  1. package/README.md +152 -9
  2. package/dist/approvals.d.ts +22 -6
  3. package/dist/approvals.d.ts.map +1 -1
  4. package/dist/approvals.js +56 -26
  5. package/dist/approvals.js.map +1 -1
  6. package/dist/audit.d.ts +8 -0
  7. package/dist/audit.d.ts.map +1 -1
  8. package/dist/audit.js.map +1 -1
  9. package/dist/auditCli.d.ts +2 -0
  10. package/dist/auditCli.d.ts.map +1 -1
  11. package/dist/auditCli.js +13 -10
  12. package/dist/auditCli.js.map +1 -1
  13. package/dist/cedar.d.ts +27 -0
  14. package/dist/cedar.d.ts.map +1 -0
  15. package/dist/cedar.js +134 -0
  16. package/dist/cedar.js.map +1 -0
  17. package/dist/cli.js +17 -1
  18. package/dist/cli.js.map +1 -1
  19. package/dist/config.d.ts +330 -21
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +74 -1
  22. package/dist/config.js.map +1 -1
  23. package/dist/elicitation.d.ts +25 -0
  24. package/dist/elicitation.d.ts.map +1 -0
  25. package/dist/elicitation.js +46 -0
  26. package/dist/elicitation.js.map +1 -0
  27. package/dist/gateway.d.ts +30 -5
  28. package/dist/gateway.d.ts.map +1 -1
  29. package/dist/gateway.js +96 -14
  30. package/dist/gateway.js.map +1 -1
  31. package/dist/hooks/claudeCode.d.ts +4 -0
  32. package/dist/hooks/claudeCode.d.ts.map +1 -1
  33. package/dist/hooks/claudeCode.js +8 -1
  34. package/dist/hooks/claudeCode.js.map +1 -1
  35. package/dist/hooks/run.d.ts.map +1 -1
  36. package/dist/hooks/run.js +6 -3
  37. package/dist/hooks/run.js.map +1 -1
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +6 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/init/presets.d.ts.map +1 -1
  43. package/dist/init/presets.js +54 -0
  44. package/dist/init/presets.js.map +1 -1
  45. package/dist/invoker.d.ts +21 -1
  46. package/dist/invoker.d.ts.map +1 -1
  47. package/dist/invoker.js +19 -2
  48. package/dist/invoker.js.map +1 -1
  49. package/dist/ledgerCli.d.ts +1 -1
  50. package/dist/ledgerCli.d.ts.map +1 -1
  51. package/dist/ledgerCli.js +9 -3
  52. package/dist/ledgerCli.js.map +1 -1
  53. package/dist/ntfy.d.ts +3 -1
  54. package/dist/ntfy.d.ts.map +1 -1
  55. package/dist/ntfy.js +2 -1
  56. package/dist/ntfy.js.map +1 -1
  57. package/dist/playgroundCli.d.ts +8 -0
  58. package/dist/playgroundCli.d.ts.map +1 -0
  59. package/dist/playgroundCli.js +57 -0
  60. package/dist/playgroundCli.js.map +1 -0
  61. package/dist/receipt.d.ts +62 -0
  62. package/dist/receipt.d.ts.map +1 -0
  63. package/dist/receipt.js +117 -0
  64. package/dist/receipt.js.map +1 -0
  65. package/dist/receiptCli.d.ts +8 -0
  66. package/dist/receiptCli.d.ts.map +1 -0
  67. package/dist/receiptCli.js +87 -0
  68. package/dist/receiptCli.js.map +1 -0
  69. package/dist/roles.d.ts +9 -0
  70. package/dist/roles.d.ts.map +1 -0
  71. package/dist/roles.js +41 -0
  72. package/dist/roles.js.map +1 -0
  73. package/dist/startup.d.ts +14 -0
  74. package/dist/startup.d.ts.map +1 -1
  75. package/dist/startup.js +92 -7
  76. package/dist/startup.js.map +1 -1
  77. package/package.json +16 -4
  78. package/playground/fake-github.mjs +98 -0
  79. package/playground/fake-memory.mjs +130 -0
  80. package/playground/index.html +411 -0
  81. package/playground/playground.yaml +40 -0
  82. package/playground/server.mjs +290 -0
  83. package/src/approvals.ts +63 -26
  84. package/src/audit.ts +5 -0
  85. package/src/auditCli.ts +11 -8
  86. package/src/cedar.ts +140 -0
  87. package/src/cli.ts +16 -1
  88. package/src/config.ts +80 -1
  89. package/src/elicitation.ts +69 -0
  90. package/src/gateway.ts +108 -17
  91. package/src/hooks/claudeCode.ts +13 -2
  92. package/src/hooks/run.ts +6 -3
  93. package/src/index.ts +6 -0
  94. package/src/init/presets.ts +54 -0
  95. package/src/invoker.ts +39 -2
  96. package/src/ledgerCli.ts +9 -3
  97. package/src/ntfy.ts +4 -1
  98. package/src/playgroundCli.ts +60 -0
  99. package/src/receipt.ts +148 -0
  100. package/src/receiptCli.ts +85 -0
  101. package/src/roles.ts +55 -0
  102. package/src/startup.ts +88 -6
@@ -0,0 +1,411 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>ADTL Playground</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f6f7f9; --panel: #ffffff; --ink: #1b1f24; --muted: #5f6b78; --line: #e3e7ec;
10
+ --accent: #3b5bdb; --ok: #2b8a3e; --ok-bg: #ebfbee; --bad: #c92a2a; --bad-bg: #fff5f5;
11
+ --warn: #b35c00; --warn-bg: #fff4e6; --chip: #eef1f5; --code: #f1f3f6;
12
+ }
13
+ @media (prefers-color-scheme: dark) {
14
+ :root {
15
+ --bg: #111418; --panel: #1a1e24; --ink: #e6e9ee; --muted: #94a0ad; --line: #2b313a;
16
+ --accent: #7c96ff; --ok: #69db7c; --ok-bg: #16281b; --bad: #ff8787; --bad-bg: #2c1717;
17
+ --warn: #ffb35c; --warn-bg: #2d2113; --chip: #252b33; --code: #151a20;
18
+ }
19
+ }
20
+ * { box-sizing: border-box; }
21
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
22
+ header { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--line); background: var(--panel); position: sticky; top: 0; z-index: 5; }
23
+ header h1 { font-size: 16px; margin: 0 12px 0 0; }
24
+ header .spacer { flex: 1; }
25
+ main { display: grid; grid-template-columns: minmax(320px, 420px) 1fr; gap: 16px; padding: 16px 20px 40px; max-width: 1400px; margin: 0 auto; }
26
+ @media (max-width: 860px) { main { grid-template-columns: 1fr; padding: 12px 16px; } }
27
+ section { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; margin-bottom: 16px; }
28
+ h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin: 0 0 10px; }
29
+ h3 { font-size: 14px; margin: 0 0 4px; }
30
+ p.hint { color: var(--muted); margin: 0 0 10px; font-size: 13px; }
31
+ button { font: inherit; border: 1px solid var(--line); background: var(--panel); color: var(--ink); border-radius: 7px; padding: 6px 12px; cursor: pointer; }
32
+ button:hover:not(:disabled) { border-color: var(--accent); }
33
+ button:disabled { opacity: .5; cursor: default; }
34
+ button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
35
+ button.allow { background: var(--ok); border-color: var(--ok); color: #fff; }
36
+ button.deny { background: var(--bad); border-color: var(--bad); color: #fff; }
37
+ .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
38
+ .seg button { border: 0; border-radius: 0; }
39
+ .seg button.on { background: var(--accent); color: #fff; }
40
+ select, textarea { width: 100%; font: 12.5px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--code); color: var(--ink); border: 1px solid var(--line); border-radius: 7px; padding: 8px; }
41
+ textarea { resize: vertical; }
42
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
43
+ .scenario { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; margin-bottom: 10px; }
44
+ .scenario.active { border-color: var(--accent); }
45
+ .scenario ol { margin: 6px 0 0; padding-left: 20px; color: var(--muted); font-size: 13px; }
46
+ .scenario li.done { color: var(--ink); }
47
+ .scenario li.now { color: var(--accent); font-weight: 600; }
48
+ .chip { display: inline-block; padding: 1px 8px; border-radius: 999px; background: var(--chip); font-size: 12px; margin: 0 4px 2px 0; white-space: nowrap; }
49
+ .chip.bad { background: var(--bad-bg); color: var(--bad); }
50
+ .chip.warn { background: var(--warn-bg); color: var(--warn); }
51
+ .chip.ok { background: var(--ok-bg); color: var(--ok); }
52
+ .label-box { display: flex; gap: 16px; flex-wrap: wrap; align-items: baseline; }
53
+ .big { font-size: 18px; font-weight: 600; }
54
+ .origins { color: var(--muted); font-size: 12px; margin-top: 6px; }
55
+ .off-banner { background: var(--warn-bg); color: var(--warn); border-radius: 8px; padding: 8px 12px; font-size: 13px; }
56
+ .ask { border: 2px solid var(--warn); background: var(--warn-bg); }
57
+ .ask .reason { margin: 4px 0 10px; }
58
+ .timeline { display: flex; flex-direction: column; gap: 8px; }
59
+ .call { border: 1px solid var(--line); border-left: 4px solid var(--line); border-radius: 8px; padding: 8px 12px; }
60
+ .call.allowed { border-left-color: var(--ok); }
61
+ .call.blocked { border-left-color: var(--bad); }
62
+ .call.pending { border-left-color: var(--warn); }
63
+ .call .head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
64
+ .call .tool { font-family: ui-monospace, Menlo, monospace; font-weight: 600; }
65
+ .call .args, .call pre { font: 12px/1.4 ui-monospace, Menlo, monospace; color: var(--muted); white-space: pre-wrap; word-break: break-word; margin: 4px 0 0; }
66
+ .call pre { background: var(--code); padding: 6px 8px; border-radius: 6px; color: var(--ink); max-height: 140px; overflow: auto; }
67
+ .divider { text-align: center; color: var(--muted); font-size: 12px; margin: 6px 0; }
68
+ .divider span { background: var(--bg); padding: 0 10px; }
69
+ .divider::before { content: ""; display: block; border-top: 1px dashed var(--line); margin-bottom: -9px; }
70
+ .leak { background: var(--bad-bg); color: var(--bad); border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; font-weight: 600; }
71
+ .safe { background: var(--ok-bg); color: var(--ok); border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; font-weight: 600; }
72
+ table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
73
+ td, th { text-align: left; padding: 5px 6px; border-bottom: 1px solid var(--line); vertical-align: top; }
74
+ th { color: var(--muted); font-weight: 500; }
75
+ .empty { color: var(--muted); font-size: 13px; }
76
+ .err { color: var(--bad); font-size: 13px; margin-top: 6px; white-space: pre-wrap; }
77
+ details summary { cursor: pointer; color: var(--muted); }
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <header>
82
+ <h1>ADTL Playground</h1>
83
+ <div class="seg" role="group" aria-label="Gateway">
84
+ <button id="mode-on">Through ADTL</button>
85
+ <button id="mode-off">Direct (no ADTL)</button>
86
+ </div>
87
+ <span class="spacer"></span>
88
+ <button id="new-session" title="Same servers, memory and ledger; a fresh session label">New session</button>
89
+ <button id="reset" title="Fresh servers, memory, ledger and audit log">Reset everything</button>
90
+ </header>
91
+
92
+ <main>
93
+ <div>
94
+ <section>
95
+ <h2>Scenarios</h2>
96
+ <p class="hint">A scripted agent that obeys any instruction it finds in tool results: the worst case. Step through it, or run it all. Then flip the gateway off and run it again.</p>
97
+ <div id="scenarios"></div>
98
+ </section>
99
+
100
+ <section>
101
+ <h2>Call a tool yourself</h2>
102
+ <select id="tool"></select>
103
+ <p class="hint" id="tool-desc" style="margin-top:6px"></p>
104
+ <textarea id="args" rows="5" spellcheck="false"></textarea>
105
+ <div class="row"><button class="primary" id="call">Call</button></div>
106
+ <div class="err" id="call-err"></div>
107
+ </section>
108
+
109
+ <section>
110
+ <h2>Labels and policy</h2>
111
+ <p class="hint">The <code>adtl.yaml</code> the gateway runs with. Apply restarts everything with it.</p>
112
+ <textarea id="config" rows="18" spellcheck="false"></textarea>
113
+ <div class="row">
114
+ <button class="primary" id="apply">Apply</button>
115
+ <button id="defaults">Restore defaults</button>
116
+ </div>
117
+ <div class="err" id="config-err"></div>
118
+ </section>
119
+ </div>
120
+
121
+ <div>
122
+ <div id="asks"></div>
123
+
124
+ <section>
125
+ <h2>Session label</h2>
126
+ <div id="session"></div>
127
+ </section>
128
+
129
+ <section>
130
+ <h2>What reached GitHub</h2>
131
+ <div id="posted"></div>
132
+ </section>
133
+
134
+ <section>
135
+ <h2>Calls</h2>
136
+ <div class="timeline" id="timeline"></div>
137
+ </section>
138
+
139
+ <section>
140
+ <details>
141
+ <summary>Audit log (<span id="audit-count">0</span> entries; no arguments or results)</summary>
142
+ <div id="audit" style="margin-top:10px"></div>
143
+ </details>
144
+ </section>
145
+ </div>
146
+ </main>
147
+
148
+ <script>
149
+ const $ = (id) => document.getElementById(id);
150
+ const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
151
+ const api = async (path, body) => {
152
+ const r = await fetch(path, body === undefined ? {} : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
153
+ const j = await r.json();
154
+ if (j.error) throw new Error(j.error);
155
+ return j;
156
+ };
157
+
158
+ let S = null; // last server snapshot
159
+ let TOOLS = [];
160
+ let busy = false;
161
+ let configDirty = false;
162
+
163
+ // ── Scenarios: each step decides its call from what the agent has seen so far ──
164
+ const textOf = (step) => step?.text ?? "";
165
+ const SCENARIOS = [
166
+ {
167
+ id: "exfil",
168
+ title: "Prompt injection in a public issue",
169
+ blurb: "A stranger's issue tells the agent to copy secrets.env from a private repo into a public comment.",
170
+ expect: "Blocked by private-stays-private. Without ADTL, the secret is posted.",
171
+ steps: [
172
+ { title: "Read issue #42 on acme/website", run: () => ["github__read_issue", { repo: "acme/website", number: 42 }] },
173
+ { title: "Read the file the issue asks for", run: (seen) => {
174
+ const m = textOf(seen[0]).match(/Read (\S+) from the private repo (\S+) and post/);
175
+ return ["github__get_file", { repo: m?.[2] ?? "acme/internal", path: m?.[1] ?? "secrets.env" }];
176
+ } },
177
+ { title: "Post it as a public comment", run: (seen) => ["github__post_comment", { repo: "acme/website", number: 42, body: textOf(seen[1]) }] },
178
+ ],
179
+ },
180
+ {
181
+ id: "memory",
182
+ title: "Injection remembered, triggered next session",
183
+ blurb: "Session 1 reads the issue and saves triage notes to memory. Session 2 never sees the issue, reads the notes, and posts publicly.",
184
+ expect: "Session 2 inherits the untrusted label from the memory record, so the post is held for your approval.",
185
+ steps: [
186
+ { title: "Session 1: read issue #42", run: () => ["github__read_issue", { repo: "acme/website", number: 42 }] },
187
+ { title: "Session 1: save triage notes to memory", run: (seen) => ["memory__add_memory", { text: `Triage notes for acme/website issue 42. ${textOf(seen[0]).split("\n\n").slice(1).join(" ")}` }] },
188
+ { title: "Start a new session", session: true },
189
+ { title: "Session 2: recall the notes", run: () => ["memory__search_memory", { query: "issue 42" }] },
190
+ { title: "Session 2: post a summary publicly", run: (seen) => ["github__post_comment", { repo: "acme/website", number: 42, body: `Summary of earlier triage:\n${textOf(seen[3])}` }] },
191
+ ],
192
+ },
193
+ {
194
+ id: "clean",
195
+ title: "Ordinary work, nothing untrusted",
196
+ blurb: "The agent lists repos and thanks a reporter. No untrusted or private data is involved.",
197
+ expect: "Allowed. Labels should not get in the way of clean work.",
198
+ steps: [
199
+ { title: "List repositories", run: () => ["github__list_repos", {}] },
200
+ { title: "Post a thank-you comment", run: () => ["github__post_comment", { repo: "acme/website", number: 7, body: "Thanks for the report! We'll take a look." }] },
201
+ ],
202
+ },
203
+ ];
204
+ const progress = {}; // scenario id → { next, seen[] }
205
+
206
+ function renderScenarios() {
207
+ $("scenarios").innerHTML = SCENARIOS.map((sc) => {
208
+ const p = progress[sc.id];
209
+ const done = p && p.next >= sc.steps.length;
210
+ return `<div class="scenario ${p && !done ? "active" : ""}">
211
+ <h3>${esc(sc.title)}</h3>
212
+ <p class="hint" style="margin:0">${esc(sc.blurb)}</p>
213
+ <ol>${sc.steps.map((st, i) => `<li class="${p && i < p.next ? "done" : p && i === p.next ? "now" : ""}">${esc(st.title)}</li>`).join("")}</ol>
214
+ <p class="hint" style="margin:6px 0 0"><b>Expected:</b> ${esc(sc.expect)}</p>
215
+ <div class="row">
216
+ <button data-step="${sc.id}" ${busy || done ? "disabled" : ""}>${p ? "Next step" : "Start (resets)"}</button>
217
+ <button data-all="${sc.id}" ${busy || done ? "disabled" : ""}>Run ${p ? "the rest" : "all"}</button>
218
+ </div>
219
+ </div>`;
220
+ }).join("");
221
+ }
222
+
223
+ async function step(id) {
224
+ const sc = SCENARIOS.find((s) => s.id === id);
225
+ if (!progress[id]) {
226
+ for (const k of Object.keys(progress)) delete progress[k];
227
+ progress[id] = { next: 0, seen: [] };
228
+ S = await api("/api/reset", {});
229
+ }
230
+ const p = progress[id];
231
+ const st = sc.steps[p.next];
232
+ if (st.session) {
233
+ S = await api("/api/session", {});
234
+ p.seen.push(null);
235
+ } else {
236
+ const [name, args] = st.run(p.seen);
237
+ p.seen.push(await callTool(name, args));
238
+ }
239
+ p.next++;
240
+ }
241
+
242
+ async function callTool(name, args) {
243
+ const r = api("/api/call", { name, args });
244
+ poll(); // show the call (and any approval) while it runs
245
+ const out = await r;
246
+ return out;
247
+ }
248
+
249
+ function guard(fn) {
250
+ return async (...a) => {
251
+ if (busy) return;
252
+ busy = true; renderScenarios();
253
+ try { await fn(...a); } catch (e) { alert(e.message); }
254
+ busy = false;
255
+ await refresh();
256
+ };
257
+ }
258
+
259
+ $("scenarios").addEventListener("click", (e) => {
260
+ const one = e.target.dataset.step, all = e.target.dataset.all;
261
+ if (one) guard(step)(one);
262
+ if (all) guard(async (id) => {
263
+ do { await step(id); await refresh(); } while (progress[id].next < SCENARIOS.find((s) => s.id === id).steps.length);
264
+ })(all);
265
+ });
266
+
267
+ // ── Free-form calls ──
268
+ function example(schema) {
269
+ const out = {};
270
+ for (const [k, v] of Object.entries(schema?.properties ?? {})) {
271
+ if (!(schema.required ?? []).includes(k)) continue;
272
+ out[k] = v.type === "number" || v.type === "integer" ? 1 : v.type === "boolean" ? false : "";
273
+ }
274
+ const presets = { repo: "acme/website", number: 42, path: "secrets.env", query: "issue", text: "a note" };
275
+ for (const k of Object.keys(out)) if (k in presets) out[k] = presets[k];
276
+ return JSON.stringify(out, null, 2);
277
+ }
278
+ function onToolChange() {
279
+ const t = TOOLS.find((x) => x.name === $("tool").value);
280
+ $("tool-desc").textContent = t?.description ?? "";
281
+ $("args").value = example(t?.inputSchema);
282
+ }
283
+ $("tool").addEventListener("change", onToolChange);
284
+ $("call").addEventListener("click", guard(async () => {
285
+ $("call-err").textContent = "";
286
+ let args;
287
+ try { args = JSON.parse($("args").value || "{}"); } catch (e) { $("call-err").textContent = `Arguments are not valid JSON: ${e.message}`; return; }
288
+ for (const k of Object.keys(progress)) delete progress[k];
289
+ await callTool($("tool").value, args);
290
+ }));
291
+
292
+ // ── Header actions ──
293
+ const restart = (body) => guard(async () => {
294
+ for (const k of Object.keys(progress)) delete progress[k];
295
+ $("config-err").textContent = "";
296
+ try { S = await api("/api/reset", body()); configDirty = false; }
297
+ catch (e) { $("config-err").textContent = e.message; }
298
+ await loadTools();
299
+ });
300
+ $("mode-on").addEventListener("click", restart(() => ({ adtl: true })));
301
+ $("mode-off").addEventListener("click", restart(() => ({ adtl: false })));
302
+ $("reset").addEventListener("click", restart(() => ({})));
303
+ $("apply").addEventListener("click", restart(() => ({ config: $("config").value })));
304
+ $("defaults").addEventListener("click", restart(() => ({ defaults: true })));
305
+ $("new-session").addEventListener("click", guard(async () => { S = await api("/api/session", {}); }));
306
+ $("config").addEventListener("input", () => { configDirty = true; });
307
+
308
+ // ── Rendering ──
309
+ function labelChips(l) {
310
+ if (!l) return "";
311
+ return `<span class="chip ${l.untrusted ? "warn" : "ok"}">${l.untrusted ? "untrusted" : "trusted"}</span>` +
312
+ `<span class="chip ${l.sensitive ? "bad" : ""}">${l.sensitive ? "sensitive" : "not sensitive"}</span>`;
313
+ }
314
+ function outcomeOf(c) {
315
+ if (c.pending) return { cls: "pending", chip: `<span class="chip warn">running…</span>` };
316
+ const a = c.audit;
317
+ if (!a) return { cls: c.isError ? "blocked" : "allowed", chip: c.isError ? `<span class="chip bad">error</span>` : `<span class="chip">ran, unchecked</span>` };
318
+ if (a.outcome === "blocked") {
319
+ const how = !a.approval ? (a.decision === "ask" ? "needs approval, blocked" : "blocked")
320
+ : a.approval.outcome === "denied" ? "denied by you" : a.approval.outcome === "timeout" ? "no answer in time, blocked" : `not approved (${a.approval.outcome})`;
321
+ return { cls: "blocked", chip: `<span class="chip bad">${esc(how)}</span>` };
322
+ }
323
+ if (a.approval) return { cls: "allowed", chip: `<span class="chip ok">approved by ${esc(a.approval.approver ?? "human")}</span>` };
324
+ return { cls: a.outcome === "forwarded" ? "allowed" : "blocked", chip: `<span class="chip ${a.outcome === "forwarded" ? "ok" : "bad"}">${esc(a.outcome === "forwarded" ? "allowed" : a.outcome)}</span>` };
325
+ }
326
+
327
+ function render() {
328
+ if (!S) return;
329
+ $("mode-on").classList.toggle("on", S.adtl);
330
+ $("mode-off").classList.toggle("on", !S.adtl);
331
+ if (!configDirty) $("config").value = S.config;
332
+ $("config").disabled = !S.adtl;
333
+
334
+ // Session label
335
+ const l = S.sessionLabel;
336
+ $("session").innerHTML = !S.adtl
337
+ ? `<div class="off-banner">ADTL is off: the agent talks to the servers directly. Nothing is labeled or checked.</div>`
338
+ : `<div class="label-box">
339
+ <div><div class="hint">session</div><div class="big">${esc(S.session)}</div></div>
340
+ <div><div class="hint">integrity</div><div class="big" style="color:var(${l?.integrity === "untrusted" ? "--warn" : "--ok"})">${esc(l?.integrity ?? "trusted")}</div></div>
341
+ <div><div class="hint">confidentiality</div><div class="big" style="color:var(${["private", "secret"].includes(l?.confidentiality) ? "--bad" : "--ok"})">${esc(l?.confidentiality ?? "public")}</div></div>
342
+ </div>
343
+ ${l?.origins?.length ? `<div class="origins">because of: ${l.origins.map((o) => `<span class="chip">${esc(o.server)}${o.tool ? ` / ${esc(o.tool)}` : ""}${o.resource ? ` / ${esc(o.resource)}` : ""}</span>`).join("")}</div>` : `<div class="origins">Nothing read yet in this session.</div>`}`;
344
+
345
+ // Approvals
346
+ $("asks").innerHTML = S.approvals.map((a) => `<section class="ask">
347
+ <h2 style="color:var(--warn)">Approval needed</h2>
348
+ <div><span class="tool" style="font-family:ui-monospace,monospace;font-weight:600">${esc(a.tool)}</span></div>
349
+ <div class="reason">${esc(a.reason)} <span class="hint">(rule: ${esc(a.matched.join(", "))})</span></div>
350
+ <div class="row"><button class="allow" data-approve="${esc(a.id)}">Allow</button><button class="deny" data-deny="${esc(a.id)}">Deny</button></div>
351
+ </section>`).join("");
352
+
353
+ // Posted
354
+ const leaked = S.posted.filter((p) => /sk_live|hunter2|AI agent/.test(p.body));
355
+ $("posted").innerHTML = (S.posted.length === 0
356
+ ? `<div class="safe">Nothing posted.</div>`
357
+ : leaked.length ? `<div class="leak">Leaked: ${leaked.length} public comment${leaked.length > 1 ? "s" : ""} carry secrets or the attacker's text.</div>` : "") +
358
+ S.posted.map((p) => `<div class="call ${/sk_live|hunter2|AI agent/.test(p.body) ? "blocked" : "allowed"}"><div class="head"><span class="tool">${esc(p.repo)}#${esc(p.number)}</span></div><pre>${esc(p.body)}</pre></div>`).join("");
359
+
360
+ // Timeline
361
+ const items = S.timeline.map((c) => {
362
+ if (c.kind === "session") return `<div class="divider"><span>${esc(c.session)} starts</span></div>`;
363
+ const o = outcomeOf(c);
364
+ const reason = c.audit?.reason ? `<div class="args">${esc(c.audit.reason)}${c.audit.matched?.length ? ` (rule: ${esc(c.audit.matched.join(", "))})` : ""}</div>` : "";
365
+ const via = [c.audit?.ledger && "ledger", c.audit?.records && "record stamp"].filter(Boolean);
366
+ return `<div class="call ${o.cls}">
367
+ <div class="head"><span class="tool">${esc(c.tool)}</span>${o.chip}${labelChips(c.label)}${via.length ? `<span class="chip warn">label from ${via.join(" + ")}</span>` : ""}</div>
368
+ <div class="args">${esc(JSON.stringify(c.args))}</div>
369
+ ${reason}
370
+ ${c.pending ? "" : `<pre>${esc(c.text)}</pre>`}
371
+ </div>`;
372
+ });
373
+ $("timeline").innerHTML = items.length > 1 ? items.reverse().join("") : `<div class="empty">No calls yet. Start a scenario or call a tool.</div>`;
374
+
375
+ // Audit
376
+ $("audit-count").textContent = S.audit.length;
377
+ $("audit").innerHTML = S.audit.length
378
+ ? `<table><tr><th>session</th><th>tool</th><th>outcome</th><th>decision</th><th>session label</th><th>reason</th></tr>${S.audit.map((e) => `<tr>
379
+ <td>${esc(e.sessionId)}</td><td>${esc(e.server)}__${esc(e.tool)}</td><td>${esc(e.outcome)}${e.approval ? ` (${esc(e.approval.outcome)})` : ""}</td>
380
+ <td>${esc(e.decision ?? "")}</td><td>${e.sessionLabel ? `${esc(e.sessionLabel.integrity)}/${esc(e.sessionLabel.confidentiality)}` : ""}</td><td>${esc(e.reason ?? "")}</td></tr>`).join("")}</table>`
381
+ : `<div class="empty">${S.adtl ? "Empty." : "No audit log without ADTL."}</div>`;
382
+
383
+ renderScenarios();
384
+ }
385
+
386
+ $("asks").addEventListener("click", async (e) => {
387
+ const id = e.target.dataset.approve ?? e.target.dataset.deny;
388
+ if (!id) return;
389
+ e.target.closest("section").querySelectorAll("button").forEach((b) => (b.disabled = true));
390
+ await api("/api/approve", { id, decision: e.target.dataset.approve ? "allow" : "deny" });
391
+ });
392
+
393
+ async function refresh() { S = await api("/api/state"); render(); }
394
+ async function loadTools() {
395
+ await refresh();
396
+ TOOLS = await api("/api/tools");
397
+ const keep = $("tool").value;
398
+ $("tool").innerHTML = TOOLS.map((t) => `<option>${esc(t.name)}</option>`).join("");
399
+ if (TOOLS.some((t) => t.name === keep)) $("tool").value = keep;
400
+ onToolChange();
401
+ await refresh();
402
+ }
403
+
404
+ // Keep the page current, so approvals show up even for a call started before a reload.
405
+ const poll = () => setTimeout(() => refresh().catch(() => {}), 150);
406
+ setInterval(() => refresh().catch(() => {}), 800);
407
+
408
+ loadTools();
409
+ </script>
410
+ </body>
411
+ </html>
@@ -0,0 +1,40 @@
1
+ # Labels and policy for the playground. Edit them in the UI and press Apply.
2
+ # The playground adds the ledger and gateway sections itself (fake GitHub and
3
+ # fake memory servers, a throwaway ledger and audit log).
4
+
5
+ defaults:
6
+ integrity: untrusted
7
+ confidentiality: private
8
+
9
+ servers:
10
+ github:
11
+ acceptServerClaims: false
12
+ tools:
13
+ read_issue: { integrity: untrusted, confidentiality: public, egress: none } # anyone can write issues
14
+ get_file: { integrity: trusted, confidentiality: private, egress: none } # private repo contents
15
+ list_repos: { integrity: trusted, confidentiality: public, egress: none }
16
+ post_comment: { integrity: trusted, confidentiality: public, egress: public } # result is just "posted"
17
+ memory:
18
+ tools:
19
+ add_memory: { integrity: trusted, confidentiality: public, egress: internal, labelInto: metadata }
20
+ update_memory: { integrity: trusted, confidentiality: public, egress: internal, labelInto: metadata }
21
+ search_memory: { integrity: trusted, confidentiality: public, egress: none, readLabels: true }
22
+ get_memory: { integrity: trusted, confidentiality: public, egress: none, readLabels: true }
23
+ consolidate: { integrity: untrusted, confidentiality: private, egress: internal } # merged records lose their stamps
24
+
25
+ policy:
26
+ default: allow
27
+ rules:
28
+ - name: no-public-write-after-untrusted
29
+ when:
30
+ session.integrity: untrusted
31
+ tool.egress: [public, external, unknown]
32
+ then: ask
33
+ reason: Session has read untrusted content; confirm before writing anywhere public
34
+
35
+ - name: private-stays-private
36
+ when:
37
+ session.confidentiality: [private, secret]
38
+ tool.egress: public
39
+ then: block
40
+ reason: Private data must not reach a public tool