flowviant 0.80.0 → 0.81.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/bin/lib/claudeAuth.mjs +164 -0
- package/bin/lib/fleet.mjs +24 -1
- package/package.json +2 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH CREDENTIAL THIS MACHINE'S CLI WILL RESOLVE — presence and expiry only,
|
|
3
|
+
* never a token value, and never a judgement about which one is correct.
|
|
4
|
+
*
|
|
5
|
+
* ── THE CONFUSION THIS EXISTS TO END ──
|
|
6
|
+
*
|
|
7
|
+
* A turn failed with the CLI's own words relayed verbatim:
|
|
8
|
+
*
|
|
9
|
+
* Failed to authenticate: OAuth session expired and could not be refreshed
|
|
10
|
+
*
|
|
11
|
+
* and the operator did the obvious thing — opened a terminal on that same
|
|
12
|
+
* machine, typed `claude`, and watched it work. Both observations were true at
|
|
13
|
+
* once, and nothing in the product could explain how, because the one fact that
|
|
14
|
+
* reconciles them is invisible from a browser: THE DAEMON'S CLI AND YOUR SHELL'S
|
|
15
|
+
* CLI DO NOT ALWAYS RESOLVE THE SAME CREDENTIAL.
|
|
16
|
+
*
|
|
17
|
+
* Two ways they diverge, and this module reports both:
|
|
18
|
+
*
|
|
19
|
+
* · THE ENVIRONMENT. `claude.mjs` spawns the CLI with `{...process.env}` on
|
|
20
|
+
* purpose — its header says so: "the CLI's own credentials live in this
|
|
21
|
+
* environment, and handing it a curated one signs it out". It also stopped
|
|
22
|
+
* deleting ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN deliberately, because on
|
|
23
|
+
* a machine the project leaves running an inherited org key is the POINT.
|
|
24
|
+
* The consequence is that an auth variable in the daemon's environment is
|
|
25
|
+
* handed to every turn — and `process.env` is a SNAPSHOT taken when the
|
|
26
|
+
* daemon started, so a variable exported weeks ago is still in there long
|
|
27
|
+
* after it left your shell. A stale one fails in a way a fresh `claude`
|
|
28
|
+
* never reproduces.
|
|
29
|
+
* · THE HOME. The CLI reads its store from $HOME. A daemon under systemd, a
|
|
30
|
+
* different user, or a container has a different one than the shell you
|
|
31
|
+
* tested in, so "I am logged in" was measured on the wrong file.
|
|
32
|
+
*
|
|
33
|
+
* ── WHAT THIS IS NOT ──
|
|
34
|
+
*
|
|
35
|
+
* NOT A POLICY. Flowviant does not pick a credential, does not prefer the
|
|
36
|
+
* subscription over a key, and does not call either one wrong — `claude.mjs`
|
|
37
|
+
* settled that: "Which credential is correct, and whether an account may be
|
|
38
|
+
* shared, is between the operator and the vendor." This reports what is
|
|
39
|
+
* PRESENT so a person can see the divergence; it never resolves it.
|
|
40
|
+
*
|
|
41
|
+
* NOT A PRECEDENCE CLAIM. Which source the CLI actually prefers is the CLI's
|
|
42
|
+
* own business and it is free to change it. We report that both exist, which is
|
|
43
|
+
* the fact that explains the symptom, and we do not assert which one won.
|
|
44
|
+
*
|
|
45
|
+
* NOT A TOKEN READER. Values never leave — not to the server, not to a log, not
|
|
46
|
+
* into an error. Presence, the variable's NAME, and the refresh expiry, which
|
|
47
|
+
* is a date.
|
|
48
|
+
*
|
|
49
|
+
* ── THE THREE-STATE RULE, WHICH THIS FILE LIVES OR DIES BY ──
|
|
50
|
+
*
|
|
51
|
+
* `source: 'unknown'` is NOT "signed out". Claude Code can keep credentials in
|
|
52
|
+
* an OS keychain (macOS especially), where a file simply does not exist and
|
|
53
|
+
* everything is fine. Reporting a confident "no login" for a machine that is
|
|
54
|
+
* working perfectly is precisely the invented state this product forbids, and
|
|
55
|
+
* it would be worse than saying nothing. Absent means absent; the surface
|
|
56
|
+
* renders nothing for it.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { readFileSync } from 'node:fs';
|
|
60
|
+
import { join } from 'node:path';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The variables that can carry auth into the CLI from the environment.
|
|
64
|
+
*
|
|
65
|
+
* The first two are the pair `claude.mjs` documents itself as deliberately NOT
|
|
66
|
+
* deleting, and both appear in childEnv.mjs's allowlist. The third is Claude
|
|
67
|
+
* Code's own headless/CI token. Names only ever leave this module — a NAME is
|
|
68
|
+
* what a person greps for, and it is not a secret.
|
|
69
|
+
*/
|
|
70
|
+
export const AUTH_ENV_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN'];
|
|
71
|
+
|
|
72
|
+
/** Claude Code's OAuth store, relative to the HOME the CLI will be handed. */
|
|
73
|
+
const CRED_REL = ['.claude', '.credentials.json'];
|
|
74
|
+
|
|
75
|
+
/** A stamp that may be seconds or milliseconds, to ISO — or null if it is
|
|
76
|
+
* neither. Same tolerance the rest of the daemon applies to numbers it did not
|
|
77
|
+
* write: an unreadable stamp is ABSENT, never zero and never "now". */
|
|
78
|
+
function isoFromStamp(v) {
|
|
79
|
+
const n = Number(v);
|
|
80
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
81
|
+
const ms = n > 1e11 ? n : n * 1000;
|
|
82
|
+
const d = new Date(ms);
|
|
83
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* What the CLI on this machine will resolve, as far as it can be seen without
|
|
88
|
+
* spending anything: no process spawned, no request made, no quota touched.
|
|
89
|
+
* Just this process's own environment and one file's metadata.
|
|
90
|
+
*
|
|
91
|
+
* `env` and `home` are parameters rather than reads of `process` so the tests
|
|
92
|
+
* can drive every branch — and so the caller passes the SAME environment the
|
|
93
|
+
* child will actually receive, which is the whole point of the report.
|
|
94
|
+
*/
|
|
95
|
+
export function claudeAuthContext({ env = process.env, home = null } = {}) {
|
|
96
|
+
const HOME = home ?? env.HOME ?? env.USERPROFILE ?? null;
|
|
97
|
+
|
|
98
|
+
// Presence, by NAME. An empty or whitespace-only value is not set: an
|
|
99
|
+
// exported-but-blank variable is a common way to try to UNSET one, and
|
|
100
|
+
// reporting it as present would send somebody hunting a variable that is
|
|
101
|
+
// doing nothing.
|
|
102
|
+
const envVars = AUTH_ENV_VARS.filter((n) => typeof env[n] === 'string' && env[n].trim() !== '');
|
|
103
|
+
|
|
104
|
+
let hasFile = false;
|
|
105
|
+
let refreshExpiresAt = null;
|
|
106
|
+
let accessExpiresAt = null;
|
|
107
|
+
let subscriptionType = null;
|
|
108
|
+
if (HOME) {
|
|
109
|
+
try {
|
|
110
|
+
const raw = readFileSync(join(HOME, ...CRED_REL), 'utf8');
|
|
111
|
+
const oauth = JSON.parse(raw)?.claudeAiOauth;
|
|
112
|
+
if (oauth && typeof oauth === 'object') {
|
|
113
|
+
hasFile = true;
|
|
114
|
+
refreshExpiresAt = isoFromStamp(oauth.refreshTokenExpiresAt);
|
|
115
|
+
accessExpiresAt = isoFromStamp(oauth.expiresAt);
|
|
116
|
+
// A plan name is not a secret and it is the one field that tells an
|
|
117
|
+
// operator WHICH kind of account the machine is spending — the thing
|
|
118
|
+
// "the machine is shared and so is its CLI login" makes everyone's
|
|
119
|
+
// business. Anything unexpected is dropped rather than relayed.
|
|
120
|
+
if (typeof oauth.subscriptionType === 'string' && oauth.subscriptionType.length <= 32) {
|
|
121
|
+
subscriptionType = oauth.subscriptionType;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Absent, unreadable, or not JSON. All three mean the same thing here:
|
|
126
|
+
// we cannot see a file, which is NOT the same as there not being a
|
|
127
|
+
// login. See the three-state rule in this file's header.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* THE SOURCE, and the honest thing to say about it.
|
|
133
|
+
*
|
|
134
|
+
* 'env' — at least one auth variable is set. Reported first because it
|
|
135
|
+
* is the state that is invisible from a shell and therefore the
|
|
136
|
+
* one that misleads; NOT because we know it wins.
|
|
137
|
+
* 'file' — no variable, and an OAuth store we could read.
|
|
138
|
+
* 'unknown' — neither. A keychain machine lives here, and so does a machine
|
|
139
|
+
* with no login at all. We cannot tell them apart, so we say
|
|
140
|
+
* nothing about which it is.
|
|
141
|
+
*/
|
|
142
|
+
const source = envVars.length ? 'env' : hasFile ? 'file' : 'unknown';
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
/** The OS user the daemon runs as — the other half of "which store". */
|
|
146
|
+
user: env.USER ?? env.LOGNAME ?? env.USERNAME ?? null,
|
|
147
|
+
home: HOME,
|
|
148
|
+
source,
|
|
149
|
+
/** NAMES ONLY. Never a value, never a prefix, never a length. */
|
|
150
|
+
envVars,
|
|
151
|
+
/** Is there a login sitting behind the variable that may be shadowing it?
|
|
152
|
+
* This — not `source` on its own — is the confusing state, and it is the
|
|
153
|
+
* only one the surface is allowed to raise unprompted. */
|
|
154
|
+
envOverridesLogin: envVars.length > 0 && hasFile,
|
|
155
|
+
/** The 22-day clock. Null when we cannot see the file, which includes every
|
|
156
|
+
* keychain machine, so null must never render as "expired". */
|
|
157
|
+
refreshExpiresAt,
|
|
158
|
+
/** The ~12-hour clock. Reported for completeness and deliberately NOT what
|
|
159
|
+
* any warning is built from: it lapses constantly and refreshes itself, so
|
|
160
|
+
* a note built on it would cry wolf twice a day. */
|
|
161
|
+
accessExpiresAt,
|
|
162
|
+
subscriptionType,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
import { createWorkManager } from './work.mjs';
|
|
86
86
|
import { scanLocalSessions, ourConversationIds } from './localSessions.mjs';
|
|
87
87
|
import { repoState } from './repoState.mjs';
|
|
88
|
+
import { claudeAuthContext } from './claudeAuth.mjs';
|
|
88
89
|
|
|
89
90
|
async function fetchRoster(
|
|
90
91
|
haveIds,
|
|
@@ -327,7 +328,29 @@ async function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
|
327
328
|
// time, so the value here is always current.
|
|
328
329
|
const state = repoState(repoRoot, baseRef);
|
|
329
330
|
if (!state) return; // not readable — say nothing rather than say "none"
|
|
330
|
-
|
|
331
|
+
/**
|
|
332
|
+
* WHICH CREDENTIAL THIS MACHINE'S CLI RESOLVES, on the same beat.
|
|
333
|
+
*
|
|
334
|
+
* It rides this report rather than getting an endpoint of its own because
|
|
335
|
+
* it is the same KIND of fact — something only the machine can see, pushed
|
|
336
|
+
* because a pull client can never be asked — and it inherits this
|
|
337
|
+
* function's three economies for free: scanned once a minute, deduped
|
|
338
|
+
* against the last ACCEPTED payload, and silent forever once an older
|
|
339
|
+
* server 404s.
|
|
340
|
+
*
|
|
341
|
+
* The dedup keeps it cheap: `claudeAuthContext` is presence plus two dates,
|
|
342
|
+
* so the string is stable across scans and only moves when something about
|
|
343
|
+
* the credential actually moves. It DOES move when the CLI refreshes its
|
|
344
|
+
* access token — that stamp is carried for diagnostics — which costs a
|
|
345
|
+
* write roughly twice a day and is the whole of the extra traffic. The
|
|
346
|
+
* WARNING is built on the 22-day refresh clock instead, so a note never
|
|
347
|
+
* fires on the ~12h cycle.
|
|
348
|
+
*
|
|
349
|
+
* NO VERSION FLOOR, and none is possible to need: this is a daemon→server
|
|
350
|
+
* report, and an older SERVER strips the unknown key in its zod parse and
|
|
351
|
+
* stores the rest exactly as before.
|
|
352
|
+
*/
|
|
353
|
+
payload = JSON.stringify({ ...state, auth: claudeAuthContext() });
|
|
331
354
|
} catch {
|
|
332
355
|
return; // a readout must never throw into the poll loop
|
|
333
356
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Run your own coding CLIs as build agents for Flowviant
|
|
3
|
+
"version": "0.81.0",
|
|
4
|
+
"description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"flowviant": "bin/cli.mjs"
|