@onepointfour-packs/flaredeck-agent 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/LICENSE.md +14 -0
- package/README.md +102 -0
- package/dist/agent.d.ts +104 -0
- package/dist/agent.js +329 -0
- package/dist/agent.js.map +1 -0
- package/dist/approval.d.ts +72 -0
- package/dist/approval.js +161 -0
- package/dist/approval.js.map +1 -0
- package/dist/approvals.d.ts +2 -0
- package/dist/approvals.js +148 -0
- package/dist/approvals.js.map +1 -0
- package/dist/auth.d.ts +79 -0
- package/dist/auth.js +181 -0
- package/dist/auth.js.map +1 -0
- package/dist/automode.d.ts +33 -0
- package/dist/automode.js +69 -0
- package/dist/automode.js.map +1 -0
- package/dist/branches.d.ts +25 -0
- package/dist/branches.js +81 -0
- package/dist/branches.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -0
- package/dist/codesearch.d.ts +27 -0
- package/dist/codesearch.js +111 -0
- package/dist/codesearch.js.map +1 -0
- package/dist/concurrency.d.ts +14 -0
- package/dist/concurrency.js +52 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.d.ts +106 -0
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -0
- package/dist/engine-antigravity.d.ts +23 -0
- package/dist/engine-antigravity.js +357 -0
- package/dist/engine-antigravity.js.map +1 -0
- package/dist/engine-claude.d.ts +18 -0
- package/dist/engine-claude.js +64 -0
- package/dist/engine-claude.js.map +1 -0
- package/dist/engine.d.ts +122 -0
- package/dist/engine.js +36 -0
- package/dist/engine.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +10 -0
- package/dist/env.js.map +1 -0
- package/dist/environment.d.ts +39 -0
- package/dist/environment.js +100 -0
- package/dist/environment.js.map +1 -0
- package/dist/flaredeck-check.d.ts +21 -0
- package/dist/flaredeck-check.js +12 -0
- package/dist/flaredeck-check.js.map +1 -0
- package/dist/flaredeck-proxy.d.ts +60 -0
- package/dist/flaredeck-proxy.js +186 -0
- package/dist/flaredeck-proxy.js.map +1 -0
- package/dist/flaredeck.d.ts +132 -0
- package/dist/flaredeck.js +106 -0
- package/dist/flaredeck.js.map +1 -0
- package/dist/gemini.d.ts +21 -0
- package/dist/gemini.js +59 -0
- package/dist/gemini.js.map +1 -0
- package/dist/gitguard.d.ts +2 -0
- package/dist/gitguard.js +90 -0
- package/dist/gitguard.js.map +1 -0
- package/dist/github.d.ts +38 -0
- package/dist/github.js +153 -0
- package/dist/github.js.map +1 -0
- package/dist/githubauth.d.ts +21 -0
- package/dist/githubauth.js +90 -0
- package/dist/githubauth.js.map +1 -0
- package/dist/intake.d.ts +124 -0
- package/dist/intake.js +383 -0
- package/dist/intake.js.map +1 -0
- package/dist/jam.d.ts +45 -0
- package/dist/jam.js +385 -0
- package/dist/jam.js.map +1 -0
- package/dist/jev.d.ts +31 -0
- package/dist/jev.js +75 -0
- package/dist/jev.js.map +1 -0
- package/dist/jevguard.d.ts +28 -0
- package/dist/jevguard.js +213 -0
- package/dist/jevguard.js.map +1 -0
- package/dist/mailbox.d.ts +22 -0
- package/dist/mailbox.js +51 -0
- package/dist/mailbox.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +764 -0
- package/dist/main.js.map +1 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.js +81 -0
- package/dist/markdown.js.map +1 -0
- package/dist/oauth.d.ts +30 -0
- package/dist/oauth.js +216 -0
- package/dist/oauth.js.map +1 -0
- package/dist/permissions.d.ts +43 -0
- package/dist/permissions.js +57 -0
- package/dist/permissions.js.map +1 -0
- package/dist/preflight.d.ts +71 -0
- package/dist/preflight.js +116 -0
- package/dist/preflight.js.map +1 -0
- package/dist/providers.d.ts +26 -0
- package/dist/providers.js +27 -0
- package/dist/providers.js.map +1 -0
- package/dist/repopicker.d.ts +3 -0
- package/dist/repopicker.js +111 -0
- package/dist/repopicker.js.map +1 -0
- package/dist/runs.d.ts +59 -0
- package/dist/runs.js +211 -0
- package/dist/runs.js.map +1 -0
- package/dist/sandbox.d.ts +13 -0
- package/dist/sandbox.js +110 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schedules.d.ts +11 -0
- package/dist/schedules.js +85 -0
- package/dist/schedules.js.map +1 -0
- package/dist/secretguard.d.ts +4 -0
- package/dist/secretguard.js +115 -0
- package/dist/secretguard.js.map +1 -0
- package/dist/secrets.d.ts +27 -0
- package/dist/secrets.js +51 -0
- package/dist/secrets.js.map +1 -0
- package/dist/serve.d.ts +17 -0
- package/dist/serve.js +1206 -0
- package/dist/serve.js.map +1 -0
- package/dist/status.d.ts +73 -0
- package/dist/status.js +98 -0
- package/dist/status.js.map +1 -0
- package/dist/stream.d.ts +91 -0
- package/dist/stream.js +103 -0
- package/dist/stream.js.map +1 -0
- package/dist/streamtoken.d.ts +2 -0
- package/dist/streamtoken.js +30 -0
- package/dist/streamtoken.js.map +1 -0
- package/dist/team.d.ts +61 -0
- package/dist/team.js +136 -0
- package/dist/team.js.map +1 -0
- package/dist/ui.d.ts +110 -0
- package/dist/ui.js +348 -0
- package/dist/ui.js.map +1 -0
- package/dist/updater.d.ts +15 -0
- package/dist/updater.js +178 -0
- package/dist/updater.js.map +1 -0
- package/dist/workspace.d.ts +115 -0
- package/dist/workspace.js +247 -0
- package/dist/workspace.js.map +1 -0
- package/dist/worktree.d.ts +42 -0
- package/dist/worktree.js +140 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +52 -0
- package/python/sidecar.py +178 -0
package/dist/approval.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
/** How long a request waits before it is treated as a refusal. */
|
|
3
|
+
export const APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
const waiting = new Map();
|
|
5
|
+
/** Called by the stream loop for every `decided` event. Returns true when a wait took it. */
|
|
6
|
+
export function deliverDecision(message) {
|
|
7
|
+
// `<requestId>:<outcome>:<what>` — see functions/src/card-sync.js notifyRequesterOnDecision.
|
|
8
|
+
const m = /^([a-z]{5}):(endorsed|rejected|allow|deny):([\s\S]*)$/.exec(message.trim());
|
|
9
|
+
if (!m)
|
|
10
|
+
return false;
|
|
11
|
+
const take = waiting.get(m[1]);
|
|
12
|
+
if (!take)
|
|
13
|
+
return false;
|
|
14
|
+
take({ outcome: m[2], by: m[3].trim() });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
/** True while any request is waiting — the stream loop uses it to know a `decided` event is expected. */
|
|
18
|
+
export const awaitingDecision = (requestId) => waiting.has(requestId);
|
|
19
|
+
/**
|
|
20
|
+
* Five lowercase letters, no `l`.
|
|
21
|
+
*
|
|
22
|
+
* Borrowed from Claude Code's own permission-relay format: `l` is excluded so the token never
|
|
23
|
+
* reads as a 1 or an I to someone typing it on a phone at three in the morning. Hex looked
|
|
24
|
+
* tidier and was worse for the one job this has.
|
|
25
|
+
*/
|
|
26
|
+
const ALPHABET = 'abcdefghijkmnopqrstuvwxyz';
|
|
27
|
+
const newToken = () => Array.from(randomBytes(5), (b) => ALPHABET[b % ALPHABET.length]).join('');
|
|
28
|
+
/** Long enough to judge the call, short enough to read on a phone. */
|
|
29
|
+
const PREVIEW_LIMIT = 1200;
|
|
30
|
+
/**
|
|
31
|
+
* What the approver is shown.
|
|
32
|
+
*
|
|
33
|
+
* This text is posted to a card, so it is read by more people than just the approver and it is
|
|
34
|
+
* stored. A command can carry a token — `curl -H "Authorization: Bearer sk-…"` — and writing that
|
|
35
|
+
* into FlareDeck would leak it into a place it was never meant to be. Claude Code masks
|
|
36
|
+
* credentials before relaying a prompt over a channel for exactly this reason; going direct means
|
|
37
|
+
* doing it here instead.
|
|
38
|
+
*/
|
|
39
|
+
const SECRET = /\b((?:sk|pk|rk|ghp|gho|ghu|ghs|ghr|github_pat|xox[abprs]|AKIA|ASIA|fd_live|fd_test)[-_A-Za-z0-9]{8,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,})/g;
|
|
40
|
+
export const redact = (text) => text.replace(SECRET, '[REDACTED]');
|
|
41
|
+
const summarise = (input) => {
|
|
42
|
+
const command = input['command'] ?? input['file_path'] ?? input['path'];
|
|
43
|
+
const text = redact(typeof command === 'string' ? command : JSON.stringify(input));
|
|
44
|
+
// A production-run SCRIPT is never cut: the reviewer and the approver are judging exactly that
|
|
45
|
+
// text, and an elided middle is where a second statement hides. The server stores it whole.
|
|
46
|
+
if (input['whole'] === true)
|
|
47
|
+
return text;
|
|
48
|
+
if (text.length <= PREVIEW_LIMIT)
|
|
49
|
+
return text;
|
|
50
|
+
// Keep both ends: the tail of a command is usually where the destination is.
|
|
51
|
+
const head = text.slice(0, PREVIEW_LIMIT - 400);
|
|
52
|
+
const tail = text.slice(-400);
|
|
53
|
+
return `${head}\n⋯ ${text.length - PREVIEW_LIMIT} characters elided ⋯\n${tail}`;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Turn a permission request into a question on the card.
|
|
57
|
+
*
|
|
58
|
+
* In auto mode the classifier has already handled everything routine, so whatever arrives here is
|
|
59
|
+
* the unusual kind — an explicit `ask` rule, or a tool that needs a person. Rather than refusing
|
|
60
|
+
* and losing the work, the harness posts it to the card and waits: anyone on the board can answer
|
|
61
|
+
* from their phone, and the reply arrives as an ordinary comment.
|
|
62
|
+
*
|
|
63
|
+
* It cannot hang. The wait ends at the timeout, or when the SDK aborts, and both mean "no".
|
|
64
|
+
*/
|
|
65
|
+
export function flaredeckDecider(deck, card, timeoutMs = APPROVAL_TIMEOUT_MS,
|
|
66
|
+
/** Routing the request carries with it: a lead who must endorse it first, and who may answer. */
|
|
67
|
+
route = {},
|
|
68
|
+
/** Called once the request is on the card, with what a local answerer needs to find it. */
|
|
69
|
+
onPosted = () => { },
|
|
70
|
+
/** Called when the lead endorses it — the wait continues, now on a person. */
|
|
71
|
+
onReviewed) {
|
|
72
|
+
return async ({ toolName, input }, { signal }) => {
|
|
73
|
+
const request = {
|
|
74
|
+
request_id: newToken(),
|
|
75
|
+
tool_name: toolName,
|
|
76
|
+
description: describe(toolName, input),
|
|
77
|
+
input_preview: summarise(input),
|
|
78
|
+
};
|
|
79
|
+
const token = request.request_id;
|
|
80
|
+
const minutes = Math.round(timeoutMs / 60_000);
|
|
81
|
+
let askedId;
|
|
82
|
+
try {
|
|
83
|
+
// The structured approval is what renders as Approve/Deny on the web and in the phone app,
|
|
84
|
+
// and what raises the push. The body text is the same request in words, so the typed
|
|
85
|
+
// `approve <id>` reply still works wherever a button isn't — email, a relay, another agent.
|
|
86
|
+
const asked = await deck.comment(card, renderRequest(request, minutes, !!route.review_by), {
|
|
87
|
+
request_id: request.request_id,
|
|
88
|
+
tool_name: request.tool_name,
|
|
89
|
+
description: request.description,
|
|
90
|
+
input_preview: request.input_preview,
|
|
91
|
+
expires_in_seconds: Math.round(timeoutMs / 1000),
|
|
92
|
+
...(route.review_by ? { review_by: route.review_by } : {}),
|
|
93
|
+
...(route.approvers?.length ? { approvers: route.approvers } : {}),
|
|
94
|
+
});
|
|
95
|
+
askedId = asked.id;
|
|
96
|
+
onPosted({ request_id: request.request_id, comment_id: asked.id });
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
// If the request itself cannot be posted, refuse — never fail open into an unreviewed call.
|
|
100
|
+
return { allow: false, reason: `Could not ask for approval (${String(e)}), so this was refused.` };
|
|
101
|
+
}
|
|
102
|
+
// Wait for the stream to hand the verdict in. An endorsement is reported and the wait goes
|
|
103
|
+
// on — the request is now with a person — so a lead's review and a person's answer are two
|
|
104
|
+
// events on one wait, not two waits.
|
|
105
|
+
let timer;
|
|
106
|
+
const final = await new Promise((resolve) => {
|
|
107
|
+
const done = (d) => { clearTimeout(timer); waiting.delete(token); signal.removeEventListener('abort', onAbort); resolve(d); };
|
|
108
|
+
const onAbort = () => done(null);
|
|
109
|
+
waiting.set(token, (d) => {
|
|
110
|
+
if (d.outcome === 'endorsed') {
|
|
111
|
+
onReviewed?.(d);
|
|
112
|
+
return;
|
|
113
|
+
} // keep waiting
|
|
114
|
+
done(d);
|
|
115
|
+
});
|
|
116
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
117
|
+
timer = setTimeout(() => done(null), timeoutMs);
|
|
118
|
+
});
|
|
119
|
+
if (final) {
|
|
120
|
+
const who = final.by || 'someone on the board';
|
|
121
|
+
if (final.outcome === 'allow')
|
|
122
|
+
return { allow: true };
|
|
123
|
+
return {
|
|
124
|
+
allow: false,
|
|
125
|
+
reason: final.outcome === 'rejected'
|
|
126
|
+
? `Your lead rejected this: ${who}. Do not resubmit it unchanged; do what the note says, or stop and say why.`
|
|
127
|
+
: `${who} declined this. Do not retry it; work around it or stop and say why.`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (signal.aborted)
|
|
131
|
+
return { allow: false, reason: 'The turn ended before anyone answered, so this did not run.' };
|
|
132
|
+
await deck
|
|
133
|
+
.comment(card, `No answer in ${minutes} minutes, so \`${toolName}\` was refused. The agent carried on without it.`)
|
|
134
|
+
.catch(() => { });
|
|
135
|
+
return {
|
|
136
|
+
allow: false,
|
|
137
|
+
reason: `Nobody approved ${toolName} within ${minutes} minutes, so it did not run. ` +
|
|
138
|
+
`Carry on with anything that does not depend on it and say clearly what is outstanding.`,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** One line of what the call does, matching the protocol's `description` field. */
|
|
143
|
+
function describe(toolName, input) {
|
|
144
|
+
const d = input['description'];
|
|
145
|
+
if (typeof d === 'string' && d.trim())
|
|
146
|
+
return redact(d.trim());
|
|
147
|
+
return toolName === 'Bash' ? 'Run shell command' : `Use ${toolName}`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The request as a card comment. Accepts `approve`/`deny` and the protocol's own `yes`/`no`, so
|
|
151
|
+
* whichever a person reaches for works — and a structured Approve/Deny button, when FlareDeck
|
|
152
|
+
* grows one, just posts the same verdict without anyone typing.
|
|
153
|
+
*/
|
|
154
|
+
function renderRequest(r, minutes, reviewed = false) {
|
|
155
|
+
return (`**Approval needed** — ${r.description} (\`${r.tool_name}\`)\n\n` +
|
|
156
|
+
(reviewed ? '_Goes to the lead for review first; the people who can approve it are asked once the lead endorses it._\n\n' : '') +
|
|
157
|
+
`\`\`\`\n${r.input_preview}\n\`\`\`\n\n` +
|
|
158
|
+
`Reply **approve ${r.request_id}** or **deny ${r.request_id}**. ` +
|
|
159
|
+
`No answer within ${minutes} minutes counts as a refusal and the agent moves on.`);
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=approval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Box, Text, render, useApp, useInput } from 'ink';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* The approvals inbox: every request agents are waiting on, answered from the terminal.
|
|
5
|
+
*
|
|
6
|
+
* The same record the phone's Approve button writes, so an answer here and an answer there are
|
|
7
|
+
* indistinguishable to the agent — it is polling the request on the card either way. What this
|
|
8
|
+
* adds is a place where all of them are visible at once, for whoever is at a keyboard when
|
|
9
|
+
* several agents are blocked.
|
|
10
|
+
*/
|
|
11
|
+
const REFRESH_MS = 10_000;
|
|
12
|
+
const age = (iso) => {
|
|
13
|
+
if (!iso)
|
|
14
|
+
return '';
|
|
15
|
+
const s = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
|
|
16
|
+
return s < 60 ? `${s}s` : s < 3600 ? `${Math.round(s / 60)}m` : `${Math.round(s / 3600)}h`;
|
|
17
|
+
};
|
|
18
|
+
const left = (iso) => {
|
|
19
|
+
const s = Math.round((Date.parse(iso) - Date.now()) / 1000);
|
|
20
|
+
return s <= 0 ? 'expired' : s < 60 ? `${s}s left` : `${Math.round(s / 60)}m left`;
|
|
21
|
+
};
|
|
22
|
+
function Inbox({ deck, me, onQuit }) {
|
|
23
|
+
const { exit } = useApp();
|
|
24
|
+
const [items, setItems] = useState([]);
|
|
25
|
+
const [selected, setSelected] = useState(0);
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const [busy, setBusy] = useState(false);
|
|
28
|
+
const [note, setNote] = useState('');
|
|
29
|
+
const [error, setError] = useState('');
|
|
30
|
+
const [, tick] = useState(0);
|
|
31
|
+
const load = async () => {
|
|
32
|
+
try {
|
|
33
|
+
const next = await deck.pendingApprovals();
|
|
34
|
+
setItems(next);
|
|
35
|
+
setSelected((i) => Math.min(i, Math.max(0, next.length - 1)));
|
|
36
|
+
setError('');
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
setError(`could not load: ${e instanceof Error ? e.message : String(e)}`);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
void load();
|
|
44
|
+
const id = setInterval(() => { void load(); tick((n) => n + 1); }, REFRESH_MS);
|
|
45
|
+
const clock = setInterval(() => tick((n) => n + 1), 1000);
|
|
46
|
+
return () => { clearInterval(id); clearInterval(clock); };
|
|
47
|
+
}, []);
|
|
48
|
+
const decide = async (state) => {
|
|
49
|
+
const it = items[selected];
|
|
50
|
+
if (!it || busy)
|
|
51
|
+
return;
|
|
52
|
+
if (it.review && it.review.state !== 'endorsed') {
|
|
53
|
+
setNote(`${it.request_id} is with the lead (${it.review.state}) — it is not yours to answer yet.`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setBusy(true);
|
|
57
|
+
try {
|
|
58
|
+
await deck.decide(it.comment_id, state);
|
|
59
|
+
setNote(`${state === 'allow' ? 'Approved' : 'Denied'} ${it.request_id} — ${it.sender_name ?? 'the agent'} ${state === 'allow' ? 'runs it now' : 'has been told'}.`);
|
|
60
|
+
setOpen(false);
|
|
61
|
+
await load();
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
setError(`could not decide: ${e instanceof Error ? e.message : String(e)}`);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
setBusy(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
useInput((input, key) => {
|
|
71
|
+
if (key.escape || input === 'q') {
|
|
72
|
+
onQuit();
|
|
73
|
+
exit();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (key.upArrow || input === 'k')
|
|
77
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
78
|
+
if (key.downArrow || input === 'j')
|
|
79
|
+
setSelected((i) => Math.min(items.length - 1, i + 1));
|
|
80
|
+
if (key.return || input === 'v')
|
|
81
|
+
setOpen((o) => !o);
|
|
82
|
+
if (input === 'r')
|
|
83
|
+
void load();
|
|
84
|
+
if (input === 'a')
|
|
85
|
+
void decide('allow');
|
|
86
|
+
if (input === 'd')
|
|
87
|
+
void decide('deny');
|
|
88
|
+
});
|
|
89
|
+
const it = items[selected];
|
|
90
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
91
|
+
React.createElement(Text, null,
|
|
92
|
+
React.createElement(Text, { bold: true }, "Approvals"),
|
|
93
|
+
React.createElement(Text, { dimColor: true },
|
|
94
|
+
" \u00B7 ",
|
|
95
|
+
items.length,
|
|
96
|
+
" waiting \u00B7 answering as ",
|
|
97
|
+
me)),
|
|
98
|
+
error ? React.createElement(Text, { color: "red" },
|
|
99
|
+
"\u26A0 ",
|
|
100
|
+
error) : null,
|
|
101
|
+
items.length === 0 ? (React.createElement(Text, { dimColor: true },
|
|
102
|
+
"Nothing is waiting. Requests appear here as agents ask; this refreshes every ",
|
|
103
|
+
REFRESH_MS / 1000,
|
|
104
|
+
"s.")) : (items.map((a, i) => (React.createElement(Box, { key: a.comment_id, flexDirection: "column" },
|
|
105
|
+
React.createElement(Text, null,
|
|
106
|
+
React.createElement(Text, { color: i === selected ? 'cyan' : undefined }, i === selected ? '▸ ' : ' '),
|
|
107
|
+
React.createElement(Text, { dimColor: true },
|
|
108
|
+
age(a.created_at).padStart(3),
|
|
109
|
+
" "),
|
|
110
|
+
React.createElement(Text, { bold: true }, a.sender_name ?? '?'),
|
|
111
|
+
React.createElement(Text, { dimColor: true },
|
|
112
|
+
" \u2192 ",
|
|
113
|
+
a.card.code ?? a.card.key),
|
|
114
|
+
React.createElement(Text, null,
|
|
115
|
+
" ",
|
|
116
|
+
a.description.length > 90 ? `${a.description.slice(0, 90)}…` : a.description),
|
|
117
|
+
React.createElement(Text, { dimColor: true },
|
|
118
|
+
" \u00B7 ",
|
|
119
|
+
left(a.expires_at)),
|
|
120
|
+
a.review ? (React.createElement(Text, { color: a.review.state === 'endorsed' ? 'green' : 'yellow' },
|
|
121
|
+
" \u00B7 ",
|
|
122
|
+
a.review.state === 'endorsed' ? `endorsed by ${a.review.decided_by_name ?? 'the lead'}` : `awaiting lead review`)) : null),
|
|
123
|
+
open && i === selected ? (React.createElement(Box, { marginLeft: 4, marginY: 0, borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1 },
|
|
124
|
+
React.createElement(Text, { dimColor: true },
|
|
125
|
+
a.card.board_name ?? '',
|
|
126
|
+
" \u00B7 ",
|
|
127
|
+
a.card.name ?? '',
|
|
128
|
+
" \u00B7 ",
|
|
129
|
+
a.tool_name,
|
|
130
|
+
" \u00B7 ",
|
|
131
|
+
a.request_id),
|
|
132
|
+
React.createElement(Text, null, a.input_preview || '(no preview)'),
|
|
133
|
+
a.review?.note ? React.createElement(Text, { color: "green" },
|
|
134
|
+
"lead: ",
|
|
135
|
+
a.review.note) : null)) : null)))),
|
|
136
|
+
note ? React.createElement(Text, { color: "green" }, note) : null,
|
|
137
|
+
React.createElement(Text, { dimColor: true },
|
|
138
|
+
busy ? 'sending… ' : '',
|
|
139
|
+
it ? 'a approve · d deny · ' : '',
|
|
140
|
+
"\u2191\u2193 select \u00B7 enter view \u00B7 r refresh \u00B7 q quit")));
|
|
141
|
+
}
|
|
142
|
+
export function startApprovals(deck, me) {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
const app = render(React.createElement(Inbox, { deck: deck, me: me, onQuit: () => resolve() }));
|
|
145
|
+
void app.waitUntilExit().then(() => resolve());
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=approvals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAC1D,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGnD;;;;;;;GAOG;AACH,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B,MAAM,GAAG,GAAG,CAAC,GAAkB,EAAU,EAAE;IACzC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;AAC7F,CAAC,CAAC;AACF,MAAM,IAAI,GAAG,CAAC,GAAW,EAAU,EAAE;IACnC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC;AACpF,CAAC,CAAC;AAEF,SAAS,KAAK,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAuD;IACtF,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAoB,EAAE,CAAC,CAAC;IAC1D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACrC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAE7B,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3C,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9D,QAAQ,CAAC,EAAE,CAAC,CAAC;QACf,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,QAAQ,CAAC,mBAAmB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,IAAI,EAAE,CAAC;QACZ,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC/E,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QAC1D,OAAO,GAAG,EAAE,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,MAAM,GAAG,KAAK,EAAE,KAAuB,EAAE,EAAE;QAC/C,MAAM,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC3B,IAAI,CAAC,EAAE,IAAI,IAAI;YAAE,OAAO;QACxB,IAAI,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YAChD,OAAO,CAAC,GAAG,EAAE,CAAC,UAAU,sBAAsB,EAAE,CAAC,MAAM,CAAC,KAAK,oCAAoC,CAAC,CAAC;YACnG,OAAO;QACT,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC;QACd,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YACxC,OAAO,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,UAAU,MAAM,EAAE,CAAC,WAAW,IAAI,WAAW,IAAI,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,GAAG,CAAC,CAAC;YACpK,OAAO,CAAC,KAAK,CAAC,CAAC;YACf,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,QAAQ,CAAC,qBAAqB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9E,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,CAAC;IAEF,QAAQ,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACtB,IAAI,GAAG,CAAC,MAAM,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;YAAC,MAAM,EAAE,CAAC;YAAC,IAAI,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC;QAC9D,IAAI,GAAG,CAAC,OAAO,IAAI,KAAK,KAAK,GAAG;YAAE,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACzE,IAAI,GAAG,CAAC,SAAS,IAAI,KAAK,KAAK,GAAG;YAAE,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1F,IAAI,GAAG,CAAC,MAAM,IAAI,KAAK,KAAK,GAAG;YAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,IAAI,KAAK,KAAK,GAAG;YAAE,KAAK,IAAI,EAAE,CAAC;QAC/B,IAAI,KAAK,KAAK,GAAG;YAAE,KAAK,MAAM,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,GAAG;YAAE,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,MAAM,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC3B,OAAO,CACL,oBAAC,GAAG,IAAC,aAAa,EAAC,QAAQ;QACzB,oBAAC,IAAI;YACH,oBAAC,IAAI,IAAC,IAAI,sBAAiB;YAC3B,oBAAC,IAAI,IAAC,QAAQ;;gBAAK,KAAK,CAAC,MAAM;;gBAA0B,EAAE,CAAQ,CAC9D;QACN,KAAK,CAAC,CAAC,CAAC,oBAAC,IAAI,IAAC,KAAK,EAAC,KAAK;;YAAI,KAAK,CAAQ,CAAC,CAAC,CAAC,IAAI;QACjD,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACpB,oBAAC,IAAI,IAAC,QAAQ;;YAA+E,UAAU,GAAG,IAAI;iBAAU,CACzH,CAAC,CAAC,CAAC,CACF,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAClB,oBAAC,GAAG,IAAC,GAAG,EAAE,CAAC,CAAC,UAAU,EAAE,aAAa,EAAC,QAAQ;YAC5C,oBAAC,IAAI;gBACH,oBAAC,IAAI,IAAC,KAAK,EAAE,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,IAAG,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAQ;gBACvF,oBAAC,IAAI,IAAC,QAAQ;oBAAE,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;wBAAS;gBACtD,oBAAC,IAAI,IAAC,IAAI,UAAE,CAAC,CAAC,WAAW,IAAI,GAAG,CAAQ;gBACxC,oBAAC,IAAI,IAAC,QAAQ;;oBAAK,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAQ;gBACpD,oBAAC,IAAI;;oBAAG,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAQ;gBAC5F,oBAAC,IAAI,IAAC,QAAQ;;oBAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAQ;gBAC5C,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CACV,oBAAC,IAAI,IAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;;oBAAM,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,eAAe,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,sBAAsB,CAAQ,CAC9L,CAAC,CAAC,CAAC,IAAI,CACH;YACN,IAAI,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CACxB,oBAAC,GAAG,IAAC,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,EAAC,OAAO,EAAC,WAAW,EAAC,MAAM,EAAC,aAAa,EAAC,QAAQ,EAAC,QAAQ,EAAE,CAAC;gBACvG,oBAAC,IAAI,IAAC,QAAQ;oBAAE,CAAC,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE;;oBAAK,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE;;oBAAK,CAAC,CAAC,SAAS;;oBAAK,CAAC,CAAC,UAAU,CAAQ;gBACtG,oBAAC,IAAI,QAAE,CAAC,CAAC,aAAa,IAAI,cAAc,CAAQ;gBAC/C,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,oBAAC,IAAI,IAAC,KAAK,EAAC,OAAO;;oBAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAQ,CAAC,CAAC,CAAC,IAAI,CACrE,CACP,CAAC,CAAC,CAAC,IAAI,CACJ,CACP,CAAC,CACH;QACA,IAAI,CAAC,CAAC,CAAC,oBAAC,IAAI,IAAC,KAAK,EAAC,OAAO,IAAE,IAAI,CAAQ,CAAC,CAAC,CAAC,IAAI;QAChD,oBAAC,IAAI,IAAC,QAAQ;YACX,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE;YACvB,EAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,EAAE;mFAC7B,CACH,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAe,EAAE,EAAU;IACxD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,MAAM,CAAC,oBAAC,KAAK,IAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,GAAI,CAAC,CAAC;QAC3E,KAAK,GAAG,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
export declare function claudeInstalled(): Promise<boolean>;
|
|
3
|
+
export declare function isRegistered(config: Config): Promise<boolean>;
|
|
4
|
+
/**
|
|
5
|
+
* Registered AND authorised — the CLI health-checks the server and reports it.
|
|
6
|
+
* Lets `login` skip the one interactive step when a machine is already set up, so re-running it
|
|
7
|
+
* is safe and cheap rather than another round of paste-the-redirect.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isConnected(config: Config): Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* Register the server with the CLI, in the agent's own repo.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately the DEFAULT `local` scope — the user's own per-project config — and not a
|
|
14
|
+
* `.mcp.json` in the repo. A `.mcp.json` can arrive with a checked-out repo, so Claude Code holds
|
|
15
|
+
* its servers "awaiting approval" until someone reviews them interactively, and `mcp login`
|
|
16
|
+
* refuses to authorise one until they do. Writing that file to make FlareDeck reachable therefore
|
|
17
|
+
* made it unreachable. Local scope needs no approval and leaves the repo untouched.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent — re-registering an existing name is not an error.
|
|
20
|
+
*/
|
|
21
|
+
export declare function register(config: Config): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Make FlareDeck visible to the agent's SESSIONS, which is a different thing from registering it.
|
|
24
|
+
*
|
|
25
|
+
* `claude mcp add` defaults to `local` scope, which lives in ~/.claude.json. The CLI reports that
|
|
26
|
+
* as Connected — but ~/.claude.json is not a filesystem *settings source*, so an SDK session with
|
|
27
|
+
* `settingSources: ['project']` never loads it. The result is the worst shape available: the CLI
|
|
28
|
+
* says everything is fine while the agent has no tools and cannot read the card it was sent.
|
|
29
|
+
*
|
|
30
|
+
* So the server goes in the repo's `.mcp.json` (project scope, which IS loaded) and is
|
|
31
|
+
* pre-approved for this project. The approval matters: a `.mcp.json` can arrive with a checked-out
|
|
32
|
+
* repo, so Claude Code holds its servers until a human reviews them — reasonable for a repo you
|
|
33
|
+
* cloned, useless for a file this program just wrote itself.
|
|
34
|
+
*/
|
|
35
|
+
export declare function ensureSessionsSeeFlaredeck(config: Config): void;
|
|
36
|
+
/**
|
|
37
|
+
* The one interactive step, and only once per machine.
|
|
38
|
+
*
|
|
39
|
+
* `--no-browser` prints the authorisation URL instead of trying to open one, which is what makes
|
|
40
|
+
* this work over SSH: open the URL on any device, sign in, and paste the redirect back. Sign in as
|
|
41
|
+
* the agent's own account (or as an admin and choose "connect as an agent") so its work is
|
|
42
|
+
* attributed to it rather than to you.
|
|
43
|
+
*/
|
|
44
|
+
/** Forget the current MCP authorisation, so the next login is a clean choice of account. */
|
|
45
|
+
export declare function logout(config: Config): Promise<void>;
|
|
46
|
+
export declare function login(config: Config): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* How this machine pays for the model. It must be the Claude subscription — metered API billing
|
|
49
|
+
* is far too expensive for agents that run unattended for hours.
|
|
50
|
+
*
|
|
51
|
+
* Nothing here has to defend against ANTHROPIC_API_KEY: `subscriptionEnv()` in agent.ts removes it
|
|
52
|
+
* from the environment the Claude process is given, so it cannot be picked up even when it is set
|
|
53
|
+
* on the box for something else. This only reports what the machine is signed in as.
|
|
54
|
+
*/
|
|
55
|
+
export type ClaudeAuth = {
|
|
56
|
+
kind: 'subscription' | 'metered' | 'none';
|
|
57
|
+
detail: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* What this machine is signed in as — asked directly, not inferred from a model call.
|
|
61
|
+
*
|
|
62
|
+
* It used to run `claude -p 'say ok' --max-turns 1` and read a non-zero exit as "not signed in".
|
|
63
|
+
* That conflates two unrelated things. A session that spends its single turn on a tool call exits
|
|
64
|
+
* 1 with `Error: Reached max turns (1)` while perfectly authenticated, so the answer depended on
|
|
65
|
+
* how the model happened to reply — and in a bare directory it reliably said "no credentials" to
|
|
66
|
+
* a machine that had them. Setup then ran again, minted a token, and the next start asked again:
|
|
67
|
+
* a loop with no exit, whose message pointed at the one thing that was not wrong.
|
|
68
|
+
*
|
|
69
|
+
* `claude auth status --json` is the question we actually mean. It costs nothing, bills nothing,
|
|
70
|
+
* and answers the same way every time. It also distinguishes a subscription from metered API
|
|
71
|
+
* billing, which matters here more than being signed in at all — an agent left running overnight
|
|
72
|
+
* on a per-token key is a bill nobody agreed to.
|
|
73
|
+
*/
|
|
74
|
+
export declare function claudeAuth(): Promise<ClaudeAuth>;
|
|
75
|
+
/**
|
|
76
|
+
* Mint a long-lived subscription token for an unattended machine. Interactive, once per box.
|
|
77
|
+
* The token goes in the environment as CLAUDE_CODE_OAUTH_TOKEN, usually via the service file.
|
|
78
|
+
*/
|
|
79
|
+
export declare function setupToken(): Promise<void>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { subscriptionEnv } from './agent.js';
|
|
6
|
+
/**
|
|
7
|
+
* Authentication is entirely the Claude CLI's job, and that is the point.
|
|
8
|
+
*
|
|
9
|
+
* The Agent SDK runs the Claude Code binary, so an MCP server registered and authorised through
|
|
10
|
+
* the CLI is the same server an SDK session sees, using the same stored credentials. The CLI
|
|
11
|
+
* refreshes the OAuth token on its own, which is why a login lasts weeks rather than the hour an
|
|
12
|
+
* access token lives. Nothing here holds a token, and no long-lived Flaredeck credential exists
|
|
13
|
+
* for someone to copy off the box.
|
|
14
|
+
*/
|
|
15
|
+
function run(command, args, opts = {}) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const child = spawn(command, args, {
|
|
18
|
+
stdio: opts.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
19
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
20
|
+
// MCP registrations and their credentials are per project, so every `claude mcp` call has
|
|
21
|
+
// to run in the repo the agent works in. Run from anywhere else and `mcp get` answers for
|
|
22
|
+
// the WRONG project — which is how a login reported success while leaving the agent's own
|
|
23
|
+
// repo unauthorised and toolless.
|
|
24
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
25
|
+
});
|
|
26
|
+
let out = '';
|
|
27
|
+
child.stdout?.on('data', (d) => (out += String(d)));
|
|
28
|
+
child.stderr?.on('data', (d) => (out += String(d)));
|
|
29
|
+
child.on('close', (code) => resolve({ code: code ?? 1, out }));
|
|
30
|
+
child.on('error', (e) => resolve({ code: 1, out: String(e) }));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function claudeInstalled() {
|
|
34
|
+
return (await run('claude', ['--version'])).code === 0;
|
|
35
|
+
}
|
|
36
|
+
export async function isRegistered(config) {
|
|
37
|
+
return (await run('claude', ['mcp', 'get', config.mcpServerName], { cwd: config.repo })).code === 0;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Registered AND authorised — the CLI health-checks the server and reports it.
|
|
41
|
+
* Lets `login` skip the one interactive step when a machine is already set up, so re-running it
|
|
42
|
+
* is safe and cheap rather than another round of paste-the-redirect.
|
|
43
|
+
*/
|
|
44
|
+
export async function isConnected(config) {
|
|
45
|
+
const { code, out } = await run('claude', ['mcp', 'get', config.mcpServerName], { cwd: config.repo });
|
|
46
|
+
return code === 0 && /Status:.*Connected/i.test(out);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register the server with the CLI, in the agent's own repo.
|
|
50
|
+
*
|
|
51
|
+
* Deliberately the DEFAULT `local` scope — the user's own per-project config — and not a
|
|
52
|
+
* `.mcp.json` in the repo. A `.mcp.json` can arrive with a checked-out repo, so Claude Code holds
|
|
53
|
+
* its servers "awaiting approval" until someone reviews them interactively, and `mcp login`
|
|
54
|
+
* refuses to authorise one until they do. Writing that file to make FlareDeck reachable therefore
|
|
55
|
+
* made it unreachable. Local scope needs no approval and leaves the repo untouched.
|
|
56
|
+
*
|
|
57
|
+
* Idempotent — re-registering an existing name is not an error.
|
|
58
|
+
*/
|
|
59
|
+
export async function register(config) {
|
|
60
|
+
if (await isRegistered(config))
|
|
61
|
+
return;
|
|
62
|
+
const { code, out } = await run('claude', [
|
|
63
|
+
'mcp', 'add', '--transport', 'http', config.mcpServerName, config.mcpUrl,
|
|
64
|
+
], { cwd: config.repo });
|
|
65
|
+
if (code !== 0)
|
|
66
|
+
throw new Error(`Could not register the MCP server:\n${out}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Make FlareDeck visible to the agent's SESSIONS, which is a different thing from registering it.
|
|
70
|
+
*
|
|
71
|
+
* `claude mcp add` defaults to `local` scope, which lives in ~/.claude.json. The CLI reports that
|
|
72
|
+
* as Connected — but ~/.claude.json is not a filesystem *settings source*, so an SDK session with
|
|
73
|
+
* `settingSources: ['project']` never loads it. The result is the worst shape available: the CLI
|
|
74
|
+
* says everything is fine while the agent has no tools and cannot read the card it was sent.
|
|
75
|
+
*
|
|
76
|
+
* So the server goes in the repo's `.mcp.json` (project scope, which IS loaded) and is
|
|
77
|
+
* pre-approved for this project. The approval matters: a `.mcp.json` can arrive with a checked-out
|
|
78
|
+
* repo, so Claude Code holds its servers until a human reviews them — reasonable for a repo you
|
|
79
|
+
* cloned, useless for a file this program just wrote itself.
|
|
80
|
+
*/
|
|
81
|
+
export function ensureSessionsSeeFlaredeck(config) {
|
|
82
|
+
const file = join(config.repo, '.mcp.json');
|
|
83
|
+
const existing = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8') || '{}') : {};
|
|
84
|
+
const servers = existing.mcpServers ?? {};
|
|
85
|
+
if (!servers[config.mcpServerName]) {
|
|
86
|
+
servers[config.mcpServerName] = { type: 'http', url: config.mcpUrl };
|
|
87
|
+
writeFileSync(file, `${JSON.stringify({ ...existing, mcpServers: servers }, null, 2)}\n`, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
// Pre-approve it for this project, or the entry sits "awaiting approval" and stays invisible.
|
|
90
|
+
const claudeJson = join(homedir(), '.claude.json');
|
|
91
|
+
if (!existsSync(claudeJson))
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
const cfg = JSON.parse(readFileSync(claudeJson, 'utf8') || '{}');
|
|
95
|
+
const project = (cfg.projects ??= {})[config.repo] ?? ((cfg.projects[config.repo] = {}));
|
|
96
|
+
const enabled = (project.enabledMcpjsonServers ??= []);
|
|
97
|
+
if (!enabled.includes(config.mcpServerName)) {
|
|
98
|
+
enabled.push(config.mcpServerName);
|
|
99
|
+
writeFileSync(claudeJson, JSON.stringify(cfg, null, 2), 'utf8');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* leave the CLI's own file alone if it is not what we expect */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The one interactive step, and only once per machine.
|
|
108
|
+
*
|
|
109
|
+
* `--no-browser` prints the authorisation URL instead of trying to open one, which is what makes
|
|
110
|
+
* this work over SSH: open the URL on any device, sign in, and paste the redirect back. Sign in as
|
|
111
|
+
* the agent's own account (or as an admin and choose "connect as an agent") so its work is
|
|
112
|
+
* attributed to it rather than to you.
|
|
113
|
+
*/
|
|
114
|
+
/** Forget the current MCP authorisation, so the next login is a clean choice of account. */
|
|
115
|
+
export async function logout(config) {
|
|
116
|
+
await run('claude', ['mcp', 'logout', config.mcpServerName], { cwd: config.repo });
|
|
117
|
+
}
|
|
118
|
+
export async function login(config) {
|
|
119
|
+
const { code } = await run('claude', ['mcp', 'login', config.mcpServerName, '--no-browser'], {
|
|
120
|
+
inherit: true,
|
|
121
|
+
cwd: config.repo,
|
|
122
|
+
});
|
|
123
|
+
if (code !== 0)
|
|
124
|
+
throw new Error('Authorisation did not complete.');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What this machine is signed in as — asked directly, not inferred from a model call.
|
|
128
|
+
*
|
|
129
|
+
* It used to run `claude -p 'say ok' --max-turns 1` and read a non-zero exit as "not signed in".
|
|
130
|
+
* That conflates two unrelated things. A session that spends its single turn on a tool call exits
|
|
131
|
+
* 1 with `Error: Reached max turns (1)` while perfectly authenticated, so the answer depended on
|
|
132
|
+
* how the model happened to reply — and in a bare directory it reliably said "no credentials" to
|
|
133
|
+
* a machine that had them. Setup then ran again, minted a token, and the next start asked again:
|
|
134
|
+
* a loop with no exit, whose message pointed at the one thing that was not wrong.
|
|
135
|
+
*
|
|
136
|
+
* `claude auth status --json` is the question we actually mean. It costs nothing, bills nothing,
|
|
137
|
+
* and answers the same way every time. It also distinguishes a subscription from metered API
|
|
138
|
+
* billing, which matters here more than being signed in at all — an agent left running overnight
|
|
139
|
+
* on a per-token key is a bill nobody agreed to.
|
|
140
|
+
*/
|
|
141
|
+
export async function claudeAuth() {
|
|
142
|
+
if (process.env['CLAUDE_CODE_OAUTH_TOKEN']) {
|
|
143
|
+
return { kind: 'subscription', detail: 'CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`)' };
|
|
144
|
+
}
|
|
145
|
+
const { code, out } = await run('claude', ['auth', 'status', '--json'], { env: subscriptionEnv() });
|
|
146
|
+
if (code !== 0) {
|
|
147
|
+
return { kind: 'none', detail: 'could not ask the Claude CLI who it is signed in as' };
|
|
148
|
+
}
|
|
149
|
+
let status;
|
|
150
|
+
try {
|
|
151
|
+
status = JSON.parse(out.slice(out.indexOf('{'), out.lastIndexOf('}') + 1));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { kind: 'none', detail: 'could not read the Claude CLI sign-in status' };
|
|
155
|
+
}
|
|
156
|
+
if (!status.loggedIn) {
|
|
157
|
+
return { kind: 'none', detail: 'no Claude credentials found — run `claude setup-token`' };
|
|
158
|
+
}
|
|
159
|
+
// Signed in, but billed per token. Say so plainly rather than starting and letting it run.
|
|
160
|
+
if (status.apiProvider && status.apiProvider !== 'firstParty') {
|
|
161
|
+
return {
|
|
162
|
+
kind: 'metered',
|
|
163
|
+
detail: `signed in via ${status.apiProvider} — metered API billing, not a subscription`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const plan = status.subscriptionType ? ` (${status.subscriptionType})` : '';
|
|
167
|
+
return {
|
|
168
|
+
kind: 'subscription',
|
|
169
|
+
detail: `signed in to the Claude CLI as ${status.authMethod ?? 'claude.ai'}${plan}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Mint a long-lived subscription token for an unattended machine. Interactive, once per box.
|
|
174
|
+
* The token goes in the environment as CLAUDE_CODE_OAUTH_TOKEN, usually via the service file.
|
|
175
|
+
*/
|
|
176
|
+
export async function setupToken() {
|
|
177
|
+
const { code } = await run('claude', ['setup-token'], { inherit: true });
|
|
178
|
+
if (code !== 0)
|
|
179
|
+
throw new Error('Could not set up a Claude token.');
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG7C;;;;;;;;GAQG;AAEH,SAAS,GAAG,CACV,OAAe,EACf,IAAc,EACd,OAAsF,EAAE;IAExF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;YACjC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;YAC5D,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtC,0FAA0F;YAC1F,0FAA0F;YAC1F,0FAA0F;YAC1F,kCAAkC;YAClC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvC,CAAC,CAAC;QACH,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QAC/D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,OAAO,CAAC,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAc;IAC/C,OAAO,CAAC,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;AACtG,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC9C,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACtG,OAAO,IAAI,KAAK,CAAC,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAc;IAC3C,IAAI,MAAM,YAAY,CAAC,MAAM,CAAC;QAAE,OAAO;IACvC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE;QACxC,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM;KACzE,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,GAAG,EAAE,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAAc;IACvD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAQ,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7F,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,IAAI,EAAE,CAAC;IAC1C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;QACnC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;QACrE,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpG,CAAC;IAED,8FAA8F;IAC9F,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;IACnD,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,QAAQ,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACzF,MAAM,OAAO,GAAa,CAAC,OAAO,CAAC,qBAAqB,KAAK,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;YAC5C,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YACnC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,4FAA4F;AAC5F,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,MAAc;IACzC,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,MAAc;IACxC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,aAAa,EAAE,cAAc,CAAC,EAAE;QAC3F,OAAO,EAAE,IAAI;QACb,GAAG,EAAE,MAAM,CAAC,IAAI;KACjB,CAAC,CAAC;IACH,IAAI,IAAI,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;AACrE,CAAC;AAYD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,IAAI,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,qDAAqD,EAAE,CAAC;IACjG,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,eAAe,EAAE,EAAE,CAAC,CAAC;IACpG,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,qDAAqD,EAAE,CAAC;IACzF,CAAC;IACD,IAAI,MAAoG,CAAC;IACzG,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,8CAA8C,EAAE,CAAC;IAClF,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,wDAAwD,EAAE,CAAC;IAC5F,CAAC;IACD,2FAA2F;IAC3F,IAAI,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,KAAK,YAAY,EAAE,CAAC;QAC9D,OAAO;YACL,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,iBAAiB,MAAM,CAAC,WAAW,4CAA4C;SACxF,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5E,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,MAAM,EAAE,kCAAkC,MAAM,CAAC,UAAU,IAAI,WAAW,GAAG,IAAI,EAAE;KACpF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACzE,IAAI,IAAI,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Config } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the auto-mode classifier is told about this machine.
|
|
4
|
+
*
|
|
5
|
+
* In auto mode a second model reviews each action instead of a human. By default it trusts only
|
|
6
|
+
* the working directory and the repo's own remotes, so ordinary agent work — pushing a branch,
|
|
7
|
+
* reaching the Flaredeck API, resetting a scratch database — looks like it could be exfiltration
|
|
8
|
+
* and gets blocked. Describing the environment is how the judge stops guessing.
|
|
9
|
+
*
|
|
10
|
+
* Entries are prose, not patterns: "write them the way you would describe your infrastructure to
|
|
11
|
+
* a new engineer". `$defaults` splices the built-in entries back in — omitting it would silently
|
|
12
|
+
* discard them, including the built-in data-exfiltration rule.
|
|
13
|
+
*/
|
|
14
|
+
export type Environment = {
|
|
15
|
+
organization: string;
|
|
16
|
+
/** Hosts the agent legitimately talks to. */
|
|
17
|
+
trustedDomains: string[];
|
|
18
|
+
/** Branches, hosts or namespaces that count as production. */
|
|
19
|
+
sensitiveTargets: string[];
|
|
20
|
+
/** True when this box is a dedicated, disposable sandbox rather than someone's laptop. */
|
|
21
|
+
sandboxed: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare const DEFAULT_ENVIRONMENT: Environment;
|
|
24
|
+
/**
|
|
25
|
+
* The settings the agent runs under, written OUTSIDE the repo on purpose.
|
|
26
|
+
*
|
|
27
|
+
* The classifier deliberately refuses to read `autoMode` from a repo's `.claude/settings.json`,
|
|
28
|
+
* "so a checked-in repo or a build step could not otherwise inject its own allow rules". The same
|
|
29
|
+
* reasoning applies here with more force: the agent can edit its own checkout, so any policy kept
|
|
30
|
+
* there is policy the agent could rewrite. Keeping it beside the agent's config means the thing
|
|
31
|
+
* being governed cannot reach the thing governing it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function writeAutoModeSettings(config: Config, env?: Environment): string;
|