@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/sandbox.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Keep a card's changes inside that card's checkout.
|
|
4
|
+
*
|
|
5
|
+
* The harness makes a worktree per card and starts the agent in it, and for a long time that was
|
|
6
|
+
* the whole story — which is placement, not confinement. A shell can `cd` out of it, `git -C` past
|
|
7
|
+
* it, or edit by absolute path, and an agent with four repositories in one folder has somewhere to
|
|
8
|
+
* go. One did: given a card with no worktree it read production out of a worktree belonging to
|
|
9
|
+
* somebody else. Nothing was harmed because that card only read, and that is luck rather than
|
|
10
|
+
* design.
|
|
11
|
+
*
|
|
12
|
+
* So: WRITE where your job is, READ wherever you like. Reading the neighbouring repositories is
|
|
13
|
+
* how an agent understands how a change fits; writing to them is how two agents end up in each
|
|
14
|
+
* other's work. The split is deliberate and the refusal says which half it is.
|
|
15
|
+
*
|
|
16
|
+
* Like gitGuard, this reads strings and is therefore not airtight — a write buried in a script it
|
|
17
|
+
* will not catch. It is the closed front door. The fence is the dead `origin` pushurl, which no
|
|
18
|
+
* amount of spelling gets past, and branch protection on the remote.
|
|
19
|
+
*/
|
|
20
|
+
/** Writing verbs. `git add` is included: staging outside your job is the first half of committing there. */
|
|
21
|
+
const GIT_WRITES = /\b(commit|add|rm|mv|restore|checkout|switch|reset|revert|stash|clean|apply|am|cherry-pick|rebase|merge|tag|branch|worktree|config)\b/;
|
|
22
|
+
/** Shell commands that change files. Deliberately short: the point is the obvious ones, said clearly. */
|
|
23
|
+
const FILE_WRITES = /(^|[\s;&|(])(rm|mv|cp|touch|mkdir|rmdir|chmod|chown|ln|truncate|tee|dd|sed\s+-i|install)\b/;
|
|
24
|
+
/** `>` and `>>`, but not `2>&1`, `>&2` or a comparison inside a test. */
|
|
25
|
+
const REDIRECTS = /(^|[^\d&<>])>>?\s*[^&\s|]/;
|
|
26
|
+
/** An absolute path mentioned anywhere in the line, so a write by full path is seen. */
|
|
27
|
+
const ABSOLUTE_PATHS = /(^|[\s"'=:])(\/[^\s"':;|&)]+)/g;
|
|
28
|
+
const inside = (path, root) => {
|
|
29
|
+
const p = resolve(path);
|
|
30
|
+
const r = resolve(root);
|
|
31
|
+
return p === r || p.startsWith(`${r}/`);
|
|
32
|
+
};
|
|
33
|
+
/** `git -C <path>` / `--git-dir=<path>`: the repo a git command acts on, when it is not the cwd. */
|
|
34
|
+
function gitTarget(command) {
|
|
35
|
+
const c = /(?:^|\s)git\s[^\n;&|]*?(?:-C\s+|--git-dir[= ]|--work-tree[= ])(["']?)([^\s"']+)\1/.exec(command);
|
|
36
|
+
return c?.[2] ?? null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* True when this command looks like it writes somewhere outside `root`.
|
|
40
|
+
*
|
|
41
|
+
* Reads are never refused, so the question is only ever asked of a line that changes something.
|
|
42
|
+
*/
|
|
43
|
+
export function writesOutside(command, root) {
|
|
44
|
+
const isGit = /(^|[\s;&|(])git\b/.test(command);
|
|
45
|
+
const target = gitTarget(command);
|
|
46
|
+
if (isGit && target && GIT_WRITES.test(command) && !inside(target, root)) {
|
|
47
|
+
return `\`git -C ${target}\` would change a checkout that is not this card's.`;
|
|
48
|
+
}
|
|
49
|
+
// A `cd` out of the worktree followed by anything that writes.
|
|
50
|
+
const cd = /(?:^|[\s;&|(])cd\s+(["']?)([^\s"';&|]+)\1/.exec(command);
|
|
51
|
+
if (cd?.[2]?.startsWith('/') && !inside(cd[2], root)) {
|
|
52
|
+
if ((isGit && GIT_WRITES.test(command)) || FILE_WRITES.test(command) || REDIRECTS.test(command)) {
|
|
53
|
+
return `This changes files under ${cd[2]}, which is outside this card's checkout.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!FILE_WRITES.test(command) && !REDIRECTS.test(command))
|
|
57
|
+
return null;
|
|
58
|
+
for (const m of command.matchAll(ABSOLUTE_PATHS)) {
|
|
59
|
+
const path = m[2];
|
|
60
|
+
// /tmp, /dev and the like are scratch, not somebody's work.
|
|
61
|
+
if (/^\/(tmp|var\/folders|dev|proc|sys|private\/tmp)(\/|$)/.test(path))
|
|
62
|
+
continue;
|
|
63
|
+
if (!inside(path, root))
|
|
64
|
+
return `This writes to ${path}, which is outside this card's checkout.`;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `job()` returns the directory this card owns, or null when there is no job — in which case
|
|
70
|
+
* nothing is enforced here, because a turn with no checkout has a different rule (it is told it
|
|
71
|
+
* may change nothing at all) and the publishing tools already refuse it.
|
|
72
|
+
*/
|
|
73
|
+
export function sandboxGuard(job, onNote) {
|
|
74
|
+
const refusal = (why, root) => `${why}\n\n` +
|
|
75
|
+
`Your checkout for this card is ${root} — commit there, and publish with push_branch. ` +
|
|
76
|
+
`You may READ anything on this machine, including the other repositories, and that is often ` +
|
|
77
|
+
`how a change is understood. Changing them is what this refuses: they are other cards' work, ` +
|
|
78
|
+
`and another agent may be in them right now. If the change genuinely belongs in another ` +
|
|
79
|
+
`repository, say so on the card rather than making it.`;
|
|
80
|
+
const check = async (input) => {
|
|
81
|
+
const root = job();
|
|
82
|
+
if (!root)
|
|
83
|
+
return { continue: true };
|
|
84
|
+
const tool = String(input?.tool_name ?? '');
|
|
85
|
+
if (tool === 'Bash') {
|
|
86
|
+
const why = writesOutside(String(input?.tool_input?.command ?? ''), root);
|
|
87
|
+
if (!why)
|
|
88
|
+
return { continue: true };
|
|
89
|
+
onNote(`blocked a write outside the worktree: ${why}`);
|
|
90
|
+
return { decision: 'block', reason: refusal(why, root) };
|
|
91
|
+
}
|
|
92
|
+
// Edit / Write / NotebookEdit all name the file they change.
|
|
93
|
+
const path = String(input?.tool_input?.file_path ?? input?.tool_input?.notebook_path ?? '');
|
|
94
|
+
if (!path || inside(path, root))
|
|
95
|
+
return { continue: true };
|
|
96
|
+
if (/^\/(tmp|var\/folders|private\/tmp)(\/|$)/.test(path))
|
|
97
|
+
return { continue: true };
|
|
98
|
+
onNote(`blocked an edit outside the worktree: ${path}`);
|
|
99
|
+
return { decision: 'block', reason: refusal(`${path} is outside this card's checkout.`, root) };
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
PreToolUse: [
|
|
103
|
+
{ matcher: 'Bash', hooks: [check] },
|
|
104
|
+
{ matcher: 'Edit', hooks: [check] },
|
|
105
|
+
{ matcher: 'Write', hooks: [check] },
|
|
106
|
+
{ matcher: 'NotebookEdit', hooks: [check] },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Flaredeck } from './flaredeck.js';
|
|
2
|
+
/**
|
|
3
|
+
* Recurring tasks, held by FlareDeck rather than by this machine.
|
|
4
|
+
*
|
|
5
|
+
* An agent asks for one here; FlareDeck files the request as an approval on a card and the
|
|
6
|
+
* schedule only goes live when a person approves it. From then on FlareDeck wakes the named agent
|
|
7
|
+
* at each time — on whichever machine that agent is running — with the prompt it was given. A run
|
|
8
|
+
* more than its late window past the due time is skipped, not delayed.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SCHEDULE_TOOL_NAMES: string[];
|
|
11
|
+
export declare function scheduleTools(deck: Flaredeck | null, currentCard: () => string | null): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Recurring tasks, held by FlareDeck rather than by this machine.
|
|
5
|
+
*
|
|
6
|
+
* An agent asks for one here; FlareDeck files the request as an approval on a card and the
|
|
7
|
+
* schedule only goes live when a person approves it. From then on FlareDeck wakes the named agent
|
|
8
|
+
* at each time — on whichever machine that agent is running — with the prompt it was given. A run
|
|
9
|
+
* more than its late window past the due time is skipped, not delayed.
|
|
10
|
+
*/
|
|
11
|
+
export const SCHEDULE_TOOL_NAMES = [
|
|
12
|
+
'mcp__schedules__create_schedule',
|
|
13
|
+
'mcp__schedules__list_schedules',
|
|
14
|
+
'mcp__schedules__delete_schedule',
|
|
15
|
+
];
|
|
16
|
+
const say = (text, isError = false) => ({ content: [{ type: 'text', text }], isError });
|
|
17
|
+
export function scheduleTools(deck, currentCard) {
|
|
18
|
+
const need = () => say('This harness has no FlareDeck client of its own, so it cannot manage schedules.', true);
|
|
19
|
+
return createSdkMcpServer({
|
|
20
|
+
name: 'schedules',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
tools: [
|
|
23
|
+
tool('create_schedule', 'Ask FlareDeck to wake an agent on a recurring schedule, for example 7am on weekdays. This does ' +
|
|
24
|
+
'NOT start it: the request is posted on a card as an approval, and a person must approve it ' +
|
|
25
|
+
'before the first run. Returns at once; do not wait for the answer. The prompt is what the agent ' +
|
|
26
|
+
'is told at each run, with no memory of earlier runs, so it must be self-contained. A run that ' +
|
|
27
|
+
'is more than max_late_minutes late is skipped, not delayed. Runs must be at least 15 minutes apart.', {
|
|
28
|
+
name: z.string().describe('A short label, e.g. "Daily summary to Darren".'),
|
|
29
|
+
cron: z.string().describe('Standard 5-field cron in the given timezone, e.g. "0 7 * * 1-5" for 07:00 Monday to Friday.'),
|
|
30
|
+
timezone: z.string().describe('IANA zone, e.g. "Africa/Johannesburg". Always state it; cron means nothing without one.'),
|
|
31
|
+
prompt: z.string().describe('Exactly what the agent should do at each run, written so it needs no other context.'),
|
|
32
|
+
card: z.string().optional().describe('The card code (e.g. KD-1234) to post the approval request on. Defaults to the card you are working.'),
|
|
33
|
+
agent: z.string().optional().describe('Email or user id of the agent to wake. Defaults to you.'),
|
|
34
|
+
max_late_minutes: z.number().optional().describe('How late a run may start before it is skipped. Default 60.'),
|
|
35
|
+
}, async ({ name, cron, timezone, prompt, card, agent, max_late_minutes }) => {
|
|
36
|
+
if (!deck)
|
|
37
|
+
return need();
|
|
38
|
+
const where = card ?? currentCard();
|
|
39
|
+
if (!where)
|
|
40
|
+
return say('Say which card to post the approval request on (the card parameter): there is no card in front of you.', true);
|
|
41
|
+
try {
|
|
42
|
+
const s = await deck.createSchedule({
|
|
43
|
+
name, cron, timezone, prompt, card: where,
|
|
44
|
+
...(agent ? { agent } : {}),
|
|
45
|
+
...(max_late_minutes ? { max_late_minutes } : {}),
|
|
46
|
+
});
|
|
47
|
+
return say(s.state === 'active'
|
|
48
|
+
? `Schedule ${s.id} is live. Next run ${s.next_run_at}.`
|
|
49
|
+
: `Schedule ${s.id} is waiting for a person to approve it on ${where}. Nothing runs until they do. ` +
|
|
50
|
+
'You will not be told the answer; list_schedules shows its state.');
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return say(`Could not register the schedule: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
tool('list_schedules', 'The recurring schedules in this workspace: state (pending approval, active, denied), cron, timezone, ' +
|
|
57
|
+
'next and last run, and the agent each one wakes.', {}, async () => {
|
|
58
|
+
if (!deck)
|
|
59
|
+
return need();
|
|
60
|
+
try {
|
|
61
|
+
const all = await deck.schedules();
|
|
62
|
+
if (!all.length)
|
|
63
|
+
return say('No schedules.');
|
|
64
|
+
return say(all.map((s) => `${s.id} · ${s.name} · ${s.state} · ${s.cron} (${s.timezone}) → ${s.agent_name ?? s.agent_uid}` +
|
|
65
|
+
`${s.next_run_at ? ` · next ${s.next_run_at}` : ''}${s.last_run_at ? ` · last ${s.last_run_at}` : ''}`).join('\n'));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return say(`Could not read the schedules: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
tool('delete_schedule', 'Stop and remove a schedule you registered.', { id: z.string().describe('The schedule id from list_schedules.') }, async ({ id }) => {
|
|
72
|
+
if (!deck)
|
|
73
|
+
return need();
|
|
74
|
+
try {
|
|
75
|
+
await deck.deleteSchedule(id);
|
|
76
|
+
return say(`Removed ${id}.`);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return say(`Could not remove it: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=schedules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedules.js","sourceRoot":"","sources":["../src/schedules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HookCallbackMatcher, HookEvent } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
/** Replace every known secret value in any text with [REDACTED]. */
|
|
3
|
+
export declare function scrub(text: string, values?: Record<string, string>): string;
|
|
4
|
+
export declare function secretGuard(log: (text: string) => void): Partial<Record<HookEvent, HookCallbackMatcher[]>>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { secretValues } from './secrets.js';
|
|
2
|
+
/**
|
|
3
|
+
* How the agent uses a credential it is never allowed to read.
|
|
4
|
+
*
|
|
5
|
+
* Two halves, and the second is the one that matters.
|
|
6
|
+
*
|
|
7
|
+
* IN: the model writes `{{secret:STRIPE_KEY}}` and the PreToolUse hook replaces it with the real
|
|
8
|
+
* value as the call goes out. The transcript — which is written to disk in plain text and resent
|
|
9
|
+
* with every subsequent request — keeps only the placeholder.
|
|
10
|
+
*
|
|
11
|
+
* OUT: substitution alone is not enough and it is easy to stop there. `curl -v` echoes its own
|
|
12
|
+
* Authorization header; a failing command prints the URL it was given; an env dump prints
|
|
13
|
+
* everything. So every tool result is scrubbed of every known secret value before the model sees
|
|
14
|
+
* it. That also covers the accident nobody planned: an agent that stumbles into printing a key
|
|
15
|
+
* gets `[REDACTED]`, not a leak.
|
|
16
|
+
*
|
|
17
|
+
* A value is only ever in the executed command and in this process. It is never in the model's
|
|
18
|
+
* context, so it is never in the session file, and never in what goes back to the API.
|
|
19
|
+
*/
|
|
20
|
+
const PLACEHOLDER = /\{\{\s*secret:([A-Z][A-Z0-9_]{1,63})\s*\}\}/g;
|
|
21
|
+
/** Short values would match everywhere and turn ordinary output into confetti. */
|
|
22
|
+
const MIN_SCRUBBABLE = 8;
|
|
23
|
+
/** Replace every known secret value in any text with [REDACTED]. */
|
|
24
|
+
export function scrub(text, values = secretValues()) {
|
|
25
|
+
let out = text;
|
|
26
|
+
for (const [name, value] of Object.entries(values)) {
|
|
27
|
+
if (!value || value.length < MIN_SCRUBBABLE)
|
|
28
|
+
continue;
|
|
29
|
+
if (!out.includes(value))
|
|
30
|
+
continue;
|
|
31
|
+
out = out.split(value).join(`[REDACTED:${name}]`);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** Walk a tool result of unknown shape, scrubbing every string in it. */
|
|
36
|
+
function scrubDeep(value, values) {
|
|
37
|
+
if (typeof value === 'string')
|
|
38
|
+
return scrub(value, values);
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return value.map((v) => scrubDeep(v, values));
|
|
41
|
+
if (value && typeof value === 'object') {
|
|
42
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, scrubDeep(v, values)]));
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
export function secretGuard(log) {
|
|
47
|
+
return {
|
|
48
|
+
PreToolUse: [
|
|
49
|
+
{
|
|
50
|
+
matcher: 'Bash',
|
|
51
|
+
hooks: [
|
|
52
|
+
async (input) => {
|
|
53
|
+
const command = String(input?.tool_input?.command ?? '');
|
|
54
|
+
PLACEHOLDER.lastIndex = 0;
|
|
55
|
+
if (!PLACEHOLDER.test(command))
|
|
56
|
+
return { continue: true };
|
|
57
|
+
const values = secretValues();
|
|
58
|
+
const missing = [];
|
|
59
|
+
const used = [];
|
|
60
|
+
PLACEHOLDER.lastIndex = 0;
|
|
61
|
+
const filled = command.replace(PLACEHOLDER, (_m, name) => {
|
|
62
|
+
const value = values[name];
|
|
63
|
+
if (value === undefined) {
|
|
64
|
+
missing.push(name);
|
|
65
|
+
return _m;
|
|
66
|
+
}
|
|
67
|
+
used.push(name);
|
|
68
|
+
return value;
|
|
69
|
+
});
|
|
70
|
+
if (missing.length) {
|
|
71
|
+
return {
|
|
72
|
+
decision: 'block',
|
|
73
|
+
reason: `There is no secret called ${missing.join(', ')} on this machine.\n\n` +
|
|
74
|
+
`Available: ${Object.keys(values).sort().join(', ') || '(none)'}. ` +
|
|
75
|
+
`A person adds one with \`flaredeck-agent secret add ${missing[0]}\` — you cannot add it yourself, ` +
|
|
76
|
+
`and you will never see its value. Say on the card which credential you need and what for.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
log(`filled in ${used.join(', ')} — the value is not in this conversation`);
|
|
80
|
+
return {
|
|
81
|
+
continue: true,
|
|
82
|
+
hookSpecificOutput: {
|
|
83
|
+
hookEventName: 'PreToolUse',
|
|
84
|
+
updatedInput: { ...input.tool_input, command: filled },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
PostToolUse: [
|
|
92
|
+
{
|
|
93
|
+
// Every tool, not just Bash: a secret can come back from a file read or an MCP call too.
|
|
94
|
+
matcher: '*',
|
|
95
|
+
hooks: [
|
|
96
|
+
async (input) => {
|
|
97
|
+
const values = secretValues();
|
|
98
|
+
if (!Object.keys(values).length)
|
|
99
|
+
return { continue: true };
|
|
100
|
+
const before = JSON.stringify(input?.tool_response ?? '');
|
|
101
|
+
const after = scrubDeep(input?.tool_response, values);
|
|
102
|
+
if (JSON.stringify(after) === before)
|
|
103
|
+
return { continue: true };
|
|
104
|
+
log('a secret came back in a tool result — redacted before the model saw it');
|
|
105
|
+
return {
|
|
106
|
+
continue: true,
|
|
107
|
+
hookSpecificOutput: { hookEventName: 'PostToolUse', updatedToolOutput: after },
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=secretguard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretguard.js","sourceRoot":"","sources":["../src/secretguard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials the agent may USE but must never SEE.
|
|
3
|
+
*
|
|
4
|
+
* A key pasted into the pane is in the session transcript, on disk in plain text, and in every
|
|
5
|
+
* API request that follows for as long as that session lives. There is no taking it back. So a
|
|
6
|
+
* secret is put here by a person, in a separate process, and the agent is given only its NAME:
|
|
7
|
+
* it writes `{{secret:STRIPE_KEY}}` and the harness substitutes the value as the command runs
|
|
8
|
+
* (see secretguard.ts), then scrubs the value back out of whatever the command printed.
|
|
9
|
+
*
|
|
10
|
+
* Stored as plain text with 0600 permissions, which is exactly the posture of the two credential
|
|
11
|
+
* files already beside it — `providers.json` holds the OpenRouter key and `me.json` holds OAuth
|
|
12
|
+
* tokens. The file keeps secrets out of the TRANSCRIPT, which is the leak that cannot be undone;
|
|
13
|
+
* it does not pretend to defend a machine someone already has an account on. Moving all three to
|
|
14
|
+
* the OS keychain is a worthwhile follow-up, and it is one change for all of them, not this file
|
|
15
|
+
* diverging on its own.
|
|
16
|
+
*/
|
|
17
|
+
export type SecretStore = Record<string, string>;
|
|
18
|
+
/** A name the agent can type in a placeholder without ambiguity, and that reads as a constant. */
|
|
19
|
+
export declare const VALID_NAME: RegExp;
|
|
20
|
+
/** Names only. This is what the agent is allowed to know. */
|
|
21
|
+
export declare function secretNames(): string[];
|
|
22
|
+
/** The values, for the harness's own substitution and scrubbing. Never returned to the model. */
|
|
23
|
+
export declare function secretValues(): SecretStore;
|
|
24
|
+
export declare function setSecret(name: string, value: string): void;
|
|
25
|
+
export declare function removeSecret(name: string): boolean;
|
|
26
|
+
/** Where they live, for telling a person where to look. */
|
|
27
|
+
export declare const secretsPath: () => string;
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const file = () => join(process.env['XDG_CONFIG_HOME'] || join(homedir(), '.config'), 'flaredeck-agent', 'secrets.json');
|
|
5
|
+
/** A name the agent can type in a placeholder without ambiguity, and that reads as a constant. */
|
|
6
|
+
export const VALID_NAME = /^[A-Z][A-Z0-9_]{1,63}$/;
|
|
7
|
+
function load() {
|
|
8
|
+
try {
|
|
9
|
+
if (!existsSync(file()))
|
|
10
|
+
return {};
|
|
11
|
+
const raw = JSON.parse(readFileSync(file(), 'utf8'));
|
|
12
|
+
return Object.fromEntries(Object.entries(raw).filter(([k, v]) => VALID_NAME.test(k) && typeof v === 'string'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function save(store) {
|
|
19
|
+
const path = file();
|
|
20
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
21
|
+
writeFileSync(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
22
|
+
// writeFileSync's mode only applies when it CREATES the file: an existing one keeps whatever
|
|
23
|
+
// permissions it had, so a file that was once world-readable would stay that way silently.
|
|
24
|
+
chmodSync(path, 0o600);
|
|
25
|
+
}
|
|
26
|
+
/** Names only. This is what the agent is allowed to know. */
|
|
27
|
+
export function secretNames() {
|
|
28
|
+
return Object.keys(load()).sort();
|
|
29
|
+
}
|
|
30
|
+
/** The values, for the harness's own substitution and scrubbing. Never returned to the model. */
|
|
31
|
+
export function secretValues() {
|
|
32
|
+
return load();
|
|
33
|
+
}
|
|
34
|
+
export function setSecret(name, value) {
|
|
35
|
+
if (!VALID_NAME.test(name))
|
|
36
|
+
throw new Error(`"${name}" is not a usable name — use CAPITALS, digits and underscores, e.g. STRIPE_KEY`);
|
|
37
|
+
if (!value.trim())
|
|
38
|
+
throw new Error('That value is empty.');
|
|
39
|
+
save({ ...load(), [name]: value });
|
|
40
|
+
}
|
|
41
|
+
export function removeSecret(name) {
|
|
42
|
+
const store = load();
|
|
43
|
+
if (!(name in store))
|
|
44
|
+
return false;
|
|
45
|
+
delete store[name];
|
|
46
|
+
save(store);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** Where they live, for telling a person where to look. */
|
|
50
|
+
export const secretsPath = file;
|
|
51
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAqB1C,MAAM,IAAI,GAAG,GAAW,EAAE,CACxB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,EAAE,iBAAiB,EAAE,cAAc,CAAC,CAAC;AAExG,kGAAkG;AAClG,MAAM,CAAC,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAEnD,SAAS,IAAI;IACX,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAgB,CAAC;QACpE,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,CACpF,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,IAAI,CAAC,KAAkB;IAC9B,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC;IACpB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5E,6FAA6F;IAC7F,2FAA2F;IAC3F,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACzB,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,WAAW;IACzB,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,KAAa;IACnD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,gFAAgF,CAAC,CAAC;IACtI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC3D,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;IACrB,IAAI,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,IAAI,CAAC,KAAK,CAAC,CAAC;IACZ,OAAO,IAAI,CAAC;AACd,CAAC;AAED,2DAA2D;AAC3D,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC"}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Config } from './config.js';
|
|
2
|
+
import type { Flaredeck } from './flaredeck.js';
|
|
3
|
+
import type { Decider } from './permissions.js';
|
|
4
|
+
export type Serve = {
|
|
5
|
+
config: Config;
|
|
6
|
+
/** Returns a currently-valid bearer; refreshed on use, since the daemon outlives any token. */
|
|
7
|
+
auth: () => Promise<string>;
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
decider: Decider;
|
|
10
|
+
deck?: Flaredeck;
|
|
11
|
+
/** A PERSON's client, when someone has done `login --me` here: what answers a run from the pane. */
|
|
12
|
+
me?: Flaredeck;
|
|
13
|
+
say: (text: string) => void;
|
|
14
|
+
/** Who this machine is, for the header. */
|
|
15
|
+
agent?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function serve(opts: Serve): Promise<void>;
|