@onepointfour-packs/flaredeck-agent 0.2.38 → 0.2.40

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 (87) hide show
  1. package/dist/agent.d.ts +42 -3
  2. package/dist/agent.js +84 -6
  3. package/dist/agent.js.map +1 -1
  4. package/dist/automode.js +43 -3
  5. package/dist/automode.js.map +1 -1
  6. package/dist/branches.d.ts +12 -1
  7. package/dist/branches.js +13 -3
  8. package/dist/branches.js.map +1 -1
  9. package/dist/browser.d.ts +52 -0
  10. package/dist/browser.js +185 -17
  11. package/dist/browser.js.map +1 -1
  12. package/dist/config.d.ts +6 -0
  13. package/dist/config.js +36 -2
  14. package/dist/config.js.map +1 -1
  15. package/dist/credentials.d.ts +217 -0
  16. package/dist/credentials.js +332 -0
  17. package/dist/credentials.js.map +1 -0
  18. package/dist/engine-antigravity.js +2 -0
  19. package/dist/engine-antigravity.js.map +1 -1
  20. package/dist/engine-claude.js +14 -2
  21. package/dist/engine-claude.js.map +1 -1
  22. package/dist/engine.d.ts +19 -0
  23. package/dist/engine.js.map +1 -1
  24. package/dist/environment.d.ts +13 -1
  25. package/dist/environment.js +35 -2
  26. package/dist/environment.js.map +1 -1
  27. package/dist/envtools.d.ts +58 -0
  28. package/dist/envtools.js +151 -0
  29. package/dist/envtools.js.map +1 -0
  30. package/dist/flaredeck.d.ts +24 -0
  31. package/dist/flaredeck.js +20 -0
  32. package/dist/flaredeck.js.map +1 -1
  33. package/dist/github.d.ts +2 -0
  34. package/dist/github.js +4 -0
  35. package/dist/github.js.map +1 -1
  36. package/dist/githubauth.d.ts +11 -1
  37. package/dist/githubauth.js +82 -16
  38. package/dist/githubauth.js.map +1 -1
  39. package/dist/hydrate.d.ts +114 -0
  40. package/dist/hydrate.js +79 -0
  41. package/dist/hydrate.js.map +1 -0
  42. package/dist/limits.d.ts +184 -0
  43. package/dist/limits.js +251 -0
  44. package/dist/limits.js.map +1 -0
  45. package/dist/main.js +178 -96
  46. package/dist/main.js.map +1 -1
  47. package/dist/picker.js +50 -4
  48. package/dist/picker.js.map +1 -1
  49. package/dist/preflight.d.ts +43 -0
  50. package/dist/preflight.js +133 -19
  51. package/dist/preflight.js.map +1 -1
  52. package/dist/providers.d.ts +10 -0
  53. package/dist/providers.js +23 -0
  54. package/dist/providers.js.map +1 -1
  55. package/dist/runs.d.ts +11 -3
  56. package/dist/runs.js +41 -4
  57. package/dist/runs.js.map +1 -1
  58. package/dist/serve.js +1061 -114
  59. package/dist/serve.js.map +1 -1
  60. package/dist/session.d.ts +51 -0
  61. package/dist/session.js +145 -0
  62. package/dist/session.js.map +1 -0
  63. package/dist/settle.d.ts +18 -0
  64. package/dist/settle.js +39 -0
  65. package/dist/settle.js.map +1 -0
  66. package/dist/signin.d.ts +47 -0
  67. package/dist/signin.js +331 -0
  68. package/dist/signin.js.map +1 -0
  69. package/dist/stream.d.ts +6 -0
  70. package/dist/stream.js +44 -3
  71. package/dist/stream.js.map +1 -1
  72. package/dist/subscriptions.d.ts +115 -0
  73. package/dist/subscriptions.js +140 -0
  74. package/dist/subscriptions.js.map +1 -0
  75. package/dist/ui.d.ts +11 -0
  76. package/dist/ui.js +6 -1
  77. package/dist/ui.js.map +1 -1
  78. package/dist/usage.d.ts +105 -0
  79. package/dist/usage.js +348 -0
  80. package/dist/usage.js.map +1 -0
  81. package/dist/wall.d.ts +35 -0
  82. package/dist/wall.js +94 -0
  83. package/dist/wall.js.map +1 -0
  84. package/dist/worktree.d.ts +19 -0
  85. package/dist/worktree.js +179 -4
  86. package/dist/worktree.js.map +1 -1
  87. package/package.json +1 -1
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Credentials an agent uses, kept in FlareDeck and NOWHERE ELSE.
3
+ *
4
+ * The complaint this answers: setting up an agent meant setting up its credentials, one box at a
5
+ * time. Connect Sceneflo. Connect Cnjure. Connect Jam. Sign in to the site under test. Then do it
6
+ * all again on the next machine, and again when the token expires. At twenty agents that is not a
7
+ * setup cost, it is a standing job.
8
+ *
9
+ * The ones that hurt most are the ones a machine cannot regenerate. A browser sign-in session is
10
+ * minutes of a person's time and sometimes a code from their phone, and it used to live in
11
+ * exactly one `~/.config/flaredeck-agent/browser/` — so the next agent that needed the same site
12
+ * signed in again from scratch, and the one after that did too.
13
+ *
14
+ * NOTHING IS WRITTEN TO THIS MACHINE. Not a copy, not a cache, not a fallback. That is a
15
+ * deliberate reversal of how this module was first built, and the reason is that a cache quietly
16
+ * undoes the entire point: twenty machines each holding a copy of a credential is the situation
17
+ * being escaped, and a "fallback" is just that situation with a nicer name. It also means
18
+ * revocation is honest — one write in FlareDeck and the next read anywhere returns nothing, with
19
+ * no window during which some disconnected box is still using what it had.
20
+ *
21
+ * The price is paid openly: an agent that cannot reach FlareDeck cannot open a signed-in page,
22
+ * and must say so rather than carrying on with something stale. `get` therefore distinguishes
23
+ * "there is no session" from "I could not ask", because those call for different behaviour — the
24
+ * first means request a sign-in, the second means stop and report.
25
+ *
26
+ * The other thing this module refuses to do is throw. A credential store that throws turns "I
27
+ * could not reach FlareDeck" into a failed card. Every path here resolves to an outcome.
28
+ */
29
+ export type CredentialKind = 'browser-session' | 'mcp-server' | 'token' | 'oauth'
30
+ /**
31
+ * A central secret that is NEVER handed to an agent — a GitHub App's private key, say. It
32
+ * exists so FlareDeck can MINT short-lived, narrowly scoped credentials from it on request.
33
+ * The server refuses to serve one of these through the ordinary read path, which is the point:
34
+ * see "the two patterns" in docs/credential-store.md.
35
+ */
36
+ | 'app-key';
37
+ export type CredentialMeta = {
38
+ id: string;
39
+ kind: CredentialKind;
40
+ label: string;
41
+ version: number;
42
+ state: 'active' | 'revoked';
43
+ bytes: number;
44
+ };
45
+ export type PutOptions = {
46
+ kind: CredentialKind;
47
+ /** What a person sees in the FlareDeck UI. Never a value, and never anything secret. */
48
+ label?: string;
49
+ };
50
+ /**
51
+ * The outcome of a read.
52
+ *
53
+ * `{ ok: true, value: null }` and `{ ok: false }` are emphatically not the same thing and the
54
+ * caller must not collapse them. The first means FlareDeck was asked and holds no session for
55
+ * this site, which is the normal case and the trigger for requesting a sign-in. The second means
56
+ * FlareDeck could not be asked, and the only correct response is to stop and say so — there is
57
+ * nothing on disk to fall back to, by design.
58
+ */
59
+ export type Read = {
60
+ ok: true;
61
+ value: Buffer | null;
62
+ } | {
63
+ ok: false;
64
+ why: string;
65
+ };
66
+ export type CredentialStore = {
67
+ get(id: string): Promise<Read>;
68
+ put(id: string, value: Buffer, opts: PutOptions): Promise<{
69
+ ok: boolean;
70
+ message?: string;
71
+ }>;
72
+ /** Metadata for everything this agent may read. Never a value. */
73
+ list(): Promise<CredentialMeta[]>;
74
+ };
75
+ /**
76
+ * What a signed-in site IS, centrally.
77
+ *
78
+ * The on-disk key was `<repo basename>/<part>` — `"App/ui"` — and the reasoning behind it is
79
+ * sound: every worktree serves the app on a different port, so a URL-keyed memory is empty on
80
+ * every card, while a given repo's `ui` is the same place on Tuesday as it was on Monday. What is
81
+ * wrong with it centrally is the subject, not the logic. "App/ui" on this machine and "App/ui" on
82
+ * another are the same project but say nothing about which SITE was signed in to, and a real
83
+ * site — Sceneflo, Cnjure, a staging box — is the same site from every machine.
84
+ *
85
+ * So both halves are kept and the subject changes:
86
+ *
87
+ * · A real site is its HOST. `session:host:app.sceneflo.ai`. The port is dropped because cookies
88
+ * ignore ports and a port is a deployment accident; the full host is kept because a session is
89
+ * scoped to a host. Two agents on two machines that both need Sceneflo now land on one key,
90
+ * which is the entire request.
91
+ * · A local site is its PROJECT. `session:repo:App:ui` for anything on loopback or a private
92
+ * address. There is no identity in `localhost:4301`; the identity is "this project's UI".
93
+ *
94
+ * The separator in the project form is a colon and not the slash the on-disk key used, because a
95
+ * credential id is a Firestore document id and a slash in one silently creates a subcollection
96
+ * somewhere nobody is looking. The server refuses it outright (`VALID_ID` in
97
+ * flaredeck-mcp/src/vault/store.ts), which is how this was caught: every local session wrote fine
98
+ * and came back 400 from FlareDeck.
99
+ *
100
+ * The tenant is not in the key — it is in the Firestore path and in the request URL, so putting
101
+ * it here would only create a second place for it to be wrong.
102
+ */
103
+ export declare function siteCredentialId(repo: string, part: string, url?: string): string;
104
+ /**
105
+ * Which site a saved session actually belongs to, read off the session itself.
106
+ *
107
+ * This exists so that `rememberSignIn` needs no new argument: its two callers are in `main.ts`
108
+ * and `serve.ts`, and those belong to somebody else. The session already knows where it came
109
+ * from, so asking it is both less code and harder to get wrong than threading a URL through.
110
+ *
111
+ * `_ss` first because `captureSessionStorage` records the origin of the page as it was when the
112
+ * session was taken — i.e. the app, after the sign-in redirect chain finished. `origins` is the
113
+ * fallback and needs the identity providers filtered out, because a Google sign-in leaves
114
+ * `accounts.google.com` in there and that is emphatically not the site being remembered.
115
+ *
116
+ * Returning undefined is a normal outcome, not a failure: the caller then keys by project.
117
+ */
118
+ export declare function sessionOrigin(session: {
119
+ origins?: unknown[];
120
+ _ss?: {
121
+ origin?: string;
122
+ }[];
123
+ } | null | undefined): string | undefined;
124
+ export type FlaredeckStoreConfig = {
125
+ tenantId: string;
126
+ /** The agent's own access token. A function because it is refreshed as it expires. */
127
+ auth: () => Promise<string>;
128
+ baseUrl?: string;
129
+ /** The board being worked, when there is one — for board-scoped credentials and the audit row. */
130
+ board?: () => string | undefined;
131
+ };
132
+ export type RemoteRead = {
133
+ kind: 'value';
134
+ value: Buffer;
135
+ meta: CredentialMeta;
136
+ } | {
137
+ kind: 'missing';
138
+ } | {
139
+ kind: 'withheld';
140
+ message: string;
141
+ } | {
142
+ kind: 'revoked';
143
+ message: string;
144
+ } | {
145
+ kind: 'unreachable';
146
+ message: string;
147
+ };
148
+ /**
149
+ * The vault over HTTP.
150
+ *
151
+ * Returns a tagged outcome rather than a value-or-null because the failures are not the same
152
+ * thing and the caller treats them differently: `missing` and `revoked` mean request a sign-in,
153
+ * `withheld` means somebody deliberately said no, `unreachable` means stop.
154
+ */
155
+ export declare class FlaredeckCredentials {
156
+ private readonly cfg;
157
+ constructor(cfg: FlaredeckStoreConfig);
158
+ private get base();
159
+ private url;
160
+ read(id: string): Promise<RemoteRead>;
161
+ write(id: string, value: Buffer, opts: PutOptions): Promise<{
162
+ ok: boolean;
163
+ message?: string;
164
+ }>;
165
+ list(): Promise<CredentialMeta[]>;
166
+ }
167
+ /**
168
+ * FlareDeck, and only FlareDeck.
169
+ *
170
+ * How many times this is called matters, because there is no cache to absorb it: once per
171
+ * `open_page`, which is a handful of times per card. That is deliberately cheaper than a cache
172
+ * would be to reason about — and it is what makes a rotation or a revoke take effect on the very
173
+ * next page an agent opens, anywhere, with nothing told to forget anything.
174
+ */
175
+ export declare function vaultStore(remote: FlaredeckCredentials, onNote?: (text: string) => void): CredentialStore;
176
+ /**
177
+ * The store for an agent with no FlareDeck login.
178
+ *
179
+ * Says why, every time, instead of silently behaving like an empty vault. The difference matters:
180
+ * an empty vault means "sign in to the site", while this means "this agent is not set up", and
181
+ * sending a person to sign in to a site over the second one wastes their time.
182
+ */
183
+ export declare function unavailable(why: string): CredentialStore;
184
+ /**
185
+ * The store for this checkout's agent, built from what is already on disk.
186
+ *
187
+ * Takes a repo path and nothing else on purpose. The browser tools are constructed from a
188
+ * `BrowserDeps` that has no FlareDeck client in it, and the places that build that object are
189
+ * several and belong to other work; threading a client through all of them to reach one call site
190
+ * would be a far bigger change than this feature deserves. The agent's login is in its config
191
+ * file, which is where every other command gets it from.
192
+ */
193
+ export declare function credentialsFor(repo: string, onNote?: (text: string) => void, board?: () => string | undefined): CredentialStore;
194
+ export declare function getJson<T>(store: CredentialStore, id: string): Promise<{
195
+ ok: true;
196
+ value: T | null;
197
+ } | {
198
+ ok: false;
199
+ why: string;
200
+ }>;
201
+ export declare function putJson(store: CredentialStore, id: string, value: unknown, opts: PutOptions): Promise<{
202
+ ok: boolean;
203
+ message?: string;
204
+ }>;
205
+ /**
206
+ * Write a credential that cannot be regenerated cheaply, with retries.
207
+ *
208
+ * A captured sign-in has nowhere else to go — there is no local copy any more — so losing it to
209
+ * one network blip would cost a person another trip through a sign-in form, possibly with a code
210
+ * from their phone. Three attempts with a widening gap, then it gives up and says so loudly; the
211
+ * card then hits the sign-in wall again on its next look and the existing machinery asks for a
212
+ * fresh one, which is the same path an expired session takes.
213
+ */
214
+ export declare function putInsistently(store: CredentialStore, id: string, value: Buffer, opts: PutOptions, onNote?: (text: string) => void, attempts?: number): Promise<{
215
+ ok: boolean;
216
+ message?: string;
217
+ }>;
@@ -0,0 +1,332 @@
1
+ import { loadConfig, saveConfig } from './config.js';
2
+ import { freshAccessToken } from './oauth.js';
3
+ /**
4
+ * How long to wait on FlareDeck.
5
+ *
6
+ * Short on read: this is in the path of opening a page, and an agent waiting half a minute to be
7
+ * told "no" is worse than being told quickly. Long on write: a captured sign-in has nowhere else
8
+ * to go, so it is worth being patient for.
9
+ */
10
+ const READ_TIMEOUT_MS = 10_000;
11
+ const WRITE_TIMEOUT_MS = 60_000;
12
+ // ---------------------------------------------------------------------------------------------
13
+ // Identity
14
+ const PRIVATE_HOST = /^(localhost|127\.\d+\.\d+\.\d+|0\.0\.0\.0|\[?::1\]?|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|169\.254\.\d+\.\d+)$/i;
15
+ const PRIVATE_SUFFIX = /\.(local|localhost|internal|test|lan|home)$/i;
16
+ /**
17
+ * What a signed-in site IS, centrally.
18
+ *
19
+ * The on-disk key was `<repo basename>/<part>` — `"App/ui"` — and the reasoning behind it is
20
+ * sound: every worktree serves the app on a different port, so a URL-keyed memory is empty on
21
+ * every card, while a given repo's `ui` is the same place on Tuesday as it was on Monday. What is
22
+ * wrong with it centrally is the subject, not the logic. "App/ui" on this machine and "App/ui" on
23
+ * another are the same project but say nothing about which SITE was signed in to, and a real
24
+ * site — Sceneflo, Cnjure, a staging box — is the same site from every machine.
25
+ *
26
+ * So both halves are kept and the subject changes:
27
+ *
28
+ * · A real site is its HOST. `session:host:app.sceneflo.ai`. The port is dropped because cookies
29
+ * ignore ports and a port is a deployment accident; the full host is kept because a session is
30
+ * scoped to a host. Two agents on two machines that both need Sceneflo now land on one key,
31
+ * which is the entire request.
32
+ * · A local site is its PROJECT. `session:repo:App:ui` for anything on loopback or a private
33
+ * address. There is no identity in `localhost:4301`; the identity is "this project's UI".
34
+ *
35
+ * The separator in the project form is a colon and not the slash the on-disk key used, because a
36
+ * credential id is a Firestore document id and a slash in one silently creates a subcollection
37
+ * somewhere nobody is looking. The server refuses it outright (`VALID_ID` in
38
+ * flaredeck-mcp/src/vault/store.ts), which is how this was caught: every local session wrote fine
39
+ * and came back 400 from FlareDeck.
40
+ *
41
+ * The tenant is not in the key — it is in the Firestore path and in the request URL, so putting
42
+ * it here would only create a second place for it to be wrong.
43
+ */
44
+ export function siteCredentialId(repo, part, url) {
45
+ const host = (() => {
46
+ if (!url)
47
+ return null;
48
+ try {
49
+ const h = new URL(url).hostname.replace(/^\[|\]$/g, '');
50
+ return PRIVATE_HOST.test(h) || PRIVATE_SUFFIX.test(h) ? null : h.toLowerCase();
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ })();
56
+ if (host)
57
+ return `session:host:${host}`;
58
+ const project = repo.replace(/\/+$/, '').split('/').pop() || repo;
59
+ return `session:repo:${project}:${part || 'ui'}`;
60
+ }
61
+ /** Well-known identity providers, which are never the site that was signed IN to. */
62
+ const IDP_HOST = /(^|\.)(accounts\.google\.com|login\.microsoftonline\.com|login\.live\.com|github\.com|auth0\.com|okta\.com|appleid\.apple\.com)$/i;
63
+ /**
64
+ * Which site a saved session actually belongs to, read off the session itself.
65
+ *
66
+ * This exists so that `rememberSignIn` needs no new argument: its two callers are in `main.ts`
67
+ * and `serve.ts`, and those belong to somebody else. The session already knows where it came
68
+ * from, so asking it is both less code and harder to get wrong than threading a URL through.
69
+ *
70
+ * `_ss` first because `captureSessionStorage` records the origin of the page as it was when the
71
+ * session was taken — i.e. the app, after the sign-in redirect chain finished. `origins` is the
72
+ * fallback and needs the identity providers filtered out, because a Google sign-in leaves
73
+ * `accounts.google.com` in there and that is emphatically not the site being remembered.
74
+ *
75
+ * Returning undefined is a normal outcome, not a failure: the caller then keys by project.
76
+ */
77
+ export function sessionOrigin(session) {
78
+ const ss = session?._ss?.find((o) => typeof o?.origin === 'string' && o.origin);
79
+ if (ss?.origin)
80
+ return ss.origin;
81
+ const origins = (session?.origins ?? [])
82
+ .map((o) => o?.origin)
83
+ .filter((o) => typeof o === 'string' && !!o)
84
+ .filter((o) => { try {
85
+ return !IDP_HOST.test(new URL(o).hostname);
86
+ }
87
+ catch {
88
+ return false;
89
+ } });
90
+ // Only when it is unambiguous. Two non-IdP origins means a guess, and a guess here splits one
91
+ // site's session across two keys, which is the exact problem this is meant to end.
92
+ return origins.length === 1 ? origins[0] : undefined;
93
+ }
94
+ /**
95
+ * The vault over HTTP.
96
+ *
97
+ * Returns a tagged outcome rather than a value-or-null because the failures are not the same
98
+ * thing and the caller treats them differently: `missing` and `revoked` mean request a sign-in,
99
+ * `withheld` means somebody deliberately said no, `unreachable` means stop.
100
+ */
101
+ export class FlaredeckCredentials {
102
+ cfg;
103
+ constructor(cfg) {
104
+ this.cfg = cfg;
105
+ }
106
+ get base() {
107
+ return (this.cfg.baseUrl ?? 'https://mcp.flaredeck.app/api').replace(/\/+$/, '');
108
+ }
109
+ url(id) {
110
+ const board = this.cfg.board?.();
111
+ return `${this.base}/v1/${encodeURIComponent(this.cfg.tenantId)}/credentials/${encodeURIComponent(id)}${board ? `?board=${encodeURIComponent(board)}` : ''}`;
112
+ }
113
+ async read(id) {
114
+ let res;
115
+ try {
116
+ res = await fetch(this.url(id), {
117
+ headers: { Authorization: `Bearer ${await this.cfg.auth()}` },
118
+ signal: AbortSignal.timeout(READ_TIMEOUT_MS),
119
+ });
120
+ }
121
+ catch (e) {
122
+ // A token refresh that failed lands here too, and it is the same situation from the agent's
123
+ // point of view: FlareDeck cannot be asked right now.
124
+ return { kind: 'unreachable', message: e instanceof Error ? e.message : String(e) };
125
+ }
126
+ const message = async () => {
127
+ const body = await res.json().catch(() => ({}));
128
+ return String(body?.message || `HTTP ${res.status}`);
129
+ };
130
+ if (res.status === 404)
131
+ return { kind: 'missing' };
132
+ if (res.status === 410)
133
+ return { kind: 'revoked', message: await message() };
134
+ if (res.status === 403)
135
+ return { kind: 'withheld', message: await message() };
136
+ if (!res.ok)
137
+ return { kind: 'unreachable', message: await message() };
138
+ const body = (await res.json().catch(() => null));
139
+ const value = Buffer.from(String(body?.value ?? ''), 'base64');
140
+ // An empty value is not a credential. Treating it as one would hand an empty session to a
141
+ // browser, which looks exactly like being signed out but without the sign-in being requested.
142
+ if (!value.length)
143
+ return { kind: 'unreachable', message: 'FlareDeck returned an empty credential' };
144
+ return {
145
+ kind: 'value',
146
+ value,
147
+ meta: {
148
+ id,
149
+ kind: body.kind ?? 'token',
150
+ label: String(body.label ?? id),
151
+ version: Number(body.version ?? 1),
152
+ state: body.state === 'revoked' ? 'revoked' : 'active',
153
+ bytes: Number(body.bytes ?? value.length),
154
+ },
155
+ };
156
+ }
157
+ async write(id, value, opts) {
158
+ try {
159
+ const res = await fetch(this.url(id), {
160
+ method: 'PUT',
161
+ headers: { Authorization: `Bearer ${await this.cfg.auth()}`, 'Content-Type': 'application/json' },
162
+ body: JSON.stringify({
163
+ kind: opts.kind,
164
+ ...(opts.label ? { label: opts.label } : {}),
165
+ value: value.toString('base64'),
166
+ }),
167
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
168
+ });
169
+ if (res.ok)
170
+ return { ok: true };
171
+ const body = await res.json().catch(() => ({}));
172
+ return { ok: false, message: String(body?.message || `HTTP ${res.status}`) };
173
+ }
174
+ catch (e) {
175
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
176
+ }
177
+ }
178
+ async list() {
179
+ try {
180
+ const res = await fetch(`${this.base}/v1/${encodeURIComponent(this.cfg.tenantId)}/credentials`, {
181
+ headers: { Authorization: `Bearer ${await this.cfg.auth()}` },
182
+ signal: AbortSignal.timeout(READ_TIMEOUT_MS),
183
+ });
184
+ if (!res.ok)
185
+ return [];
186
+ const body = (await res.json());
187
+ return (body.credentials ?? []).map((c) => ({
188
+ id: String(c.id),
189
+ kind: c.kind ?? 'token',
190
+ label: String(c.label ?? c.id),
191
+ version: Number(c.version ?? 1),
192
+ state: c.state === 'revoked' ? 'revoked' : 'active',
193
+ bytes: Number(c.bytes ?? 0),
194
+ }));
195
+ }
196
+ catch {
197
+ return [];
198
+ }
199
+ }
200
+ }
201
+ // ---------------------------------------------------------------------------------------------
202
+ // The store
203
+ /**
204
+ * FlareDeck, and only FlareDeck.
205
+ *
206
+ * How many times this is called matters, because there is no cache to absorb it: once per
207
+ * `open_page`, which is a handful of times per card. That is deliberately cheaper than a cache
208
+ * would be to reason about — and it is what makes a rotation or a revoke take effect on the very
209
+ * next page an agent opens, anywhere, with nothing told to forget anything.
210
+ */
211
+ export function vaultStore(remote, onNote = () => { }) {
212
+ return {
213
+ async get(id) {
214
+ const read = await remote.read(id);
215
+ switch (read.kind) {
216
+ case 'value':
217
+ return { ok: true, value: read.value };
218
+ case 'missing':
219
+ // Normal, and the trigger for requesting a sign-in. Not worth a note: it is what every
220
+ // first visit to a site looks like.
221
+ return { ok: true, value: null };
222
+ case 'revoked':
223
+ onNote(`${id} was revoked in FlareDeck (${read.message}) — a fresh sign-in is needed.`);
224
+ return { ok: true, value: null };
225
+ case 'withheld':
226
+ onNote(`${id} is in FlareDeck but not for this agent: ${read.message}`);
227
+ return { ok: true, value: null };
228
+ case 'unreachable':
229
+ return { ok: false, why: read.message };
230
+ }
231
+ },
232
+ put: (id, value, opts) => remote.write(id, value, opts),
233
+ list: () => remote.list(),
234
+ };
235
+ }
236
+ /**
237
+ * The store for an agent with no FlareDeck login.
238
+ *
239
+ * Says why, every time, instead of silently behaving like an empty vault. The difference matters:
240
+ * an empty vault means "sign in to the site", while this means "this agent is not set up", and
241
+ * sending a person to sign in to a site over the second one wastes their time.
242
+ */
243
+ export function unavailable(why) {
244
+ return {
245
+ async get() { return { ok: false, why }; },
246
+ async put() { return { ok: false, message: why }; },
247
+ async list() { return []; },
248
+ };
249
+ }
250
+ // ---------------------------------------------------------------------------------------------
251
+ // Wiring
252
+ /**
253
+ * The store for this checkout's agent, built from what is already on disk.
254
+ *
255
+ * Takes a repo path and nothing else on purpose. The browser tools are constructed from a
256
+ * `BrowserDeps` that has no FlareDeck client in it, and the places that build that object are
257
+ * several and belong to other work; threading a client through all of them to reach one call site
258
+ * would be a far bigger change than this feature deserves. The agent's login is in its config
259
+ * file, which is where every other command gets it from.
260
+ */
261
+ export function credentialsFor(repo, onNote = () => { }, board) {
262
+ const config = loadConfig(repo);
263
+ const tenantId = config ? tenantOfConfig(config) : null;
264
+ if (!config?.oauth || !tenantId) {
265
+ return unavailable('this agent has no FlareDeck login, and credentials are only ever read from FlareDeck — run: flaredeck-agent login');
266
+ }
267
+ const origin = new URL(config.mcpUrl).origin;
268
+ return vaultStore(new FlaredeckCredentials({
269
+ tenantId,
270
+ baseUrl: `${origin}/api`,
271
+ auth: () => freshAccessToken(origin, config.oauth, (next) => {
272
+ config.oauth = next;
273
+ saveConfig(config);
274
+ }),
275
+ ...(board ? { board } : {}),
276
+ }), onNote);
277
+ }
278
+ /** The tenant the agent's own token is for. Same read as main.ts's `tenantOf`. */
279
+ function tenantOfConfig(config) {
280
+ if (!config.oauth)
281
+ return config.tenantId ?? null;
282
+ try {
283
+ const part = config.oauth.accessToken.split('.')[1] ?? '';
284
+ return JSON.parse(Buffer.from(part, 'base64url').toString('utf8')).tenantId ?? config.tenantId ?? null;
285
+ }
286
+ catch {
287
+ return config.tenantId ?? null;
288
+ }
289
+ }
290
+ // ---------------------------------------------------------------------------------------------
291
+ // JSON, which is what everything stored here happens to be
292
+ export async function getJson(store, id) {
293
+ const read = await store.get(id);
294
+ if (!read.ok)
295
+ return read;
296
+ if (!read.value)
297
+ return { ok: true, value: null };
298
+ try {
299
+ return { ok: true, value: JSON.parse(read.value.toString('utf8')) };
300
+ }
301
+ catch {
302
+ // A corrupt or truncated credential reads as "no credential", never as a throw: the caller's
303
+ // only sensible response is to sign in again, and that is also its response to not having one.
304
+ return { ok: true, value: null };
305
+ }
306
+ }
307
+ export function putJson(store, id, value, opts) {
308
+ return store.put(id, Buffer.from(JSON.stringify(value), 'utf8'), opts);
309
+ }
310
+ /**
311
+ * Write a credential that cannot be regenerated cheaply, with retries.
312
+ *
313
+ * A captured sign-in has nowhere else to go — there is no local copy any more — so losing it to
314
+ * one network blip would cost a person another trip through a sign-in form, possibly with a code
315
+ * from their phone. Three attempts with a widening gap, then it gives up and says so loudly; the
316
+ * card then hits the sign-in wall again on its next look and the existing machinery asks for a
317
+ * fresh one, which is the same path an expired session takes.
318
+ */
319
+ export async function putInsistently(store, id, value, opts, onNote = () => { }, attempts = 3) {
320
+ let last = { ok: false, message: 'not attempted' };
321
+ for (let n = 1; n <= attempts; n++) {
322
+ last = await store.put(id, value, opts);
323
+ if (last.ok)
324
+ return last;
325
+ if (n < attempts) {
326
+ onNote(`could not save ${id} to FlareDeck (${last.message}) — retrying (${n}/${attempts - 1}).`);
327
+ await new Promise((r) => setTimeout(r, 1000 * 2 ** (n - 1)));
328
+ }
329
+ }
330
+ return last;
331
+ }
332
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
@@ -166,6 +166,7 @@ export class AntigravityEngine {
166
166
  ...(ctx.tools.runs ? { runs: ctx.tools.runs.server } : {}),
167
167
  ...(ctx.tools.schedules ? { schedules: ctx.tools.schedules.server } : {}),
168
168
  ...(ctx.tools.browser ? { browser: ctx.tools.browser.server } : {}),
169
+ ...(ctx.tools.env ? { env: ctx.tools.env.server } : {}),
169
170
  }, (note) => push({ kind: 'tool', name: note }));
170
171
  local = served;
171
172
  // FlareDeck through the harness rather than straight at the server: the SDK lists tools
@@ -276,6 +277,7 @@ export class AntigravityEngine {
276
277
  ...(ctx.tools.runs?.toolNames ?? []),
277
278
  ...(ctx.tools.schedules?.toolNames ?? []),
278
279
  ...(ctx.tools.browser?.toolNames ?? []),
280
+ ...(ctx.tools.env?.toolNames ?? []),
279
281
  ],
280
282
  ...(ctx.resume ? { resume: ctx.resume } : {}),
281
283
  // Mapped from the SIZE, never from `ctx.model` — that is the other engine's name for the