balladeer 1.0.3 → 1.0.4
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/README.md +9 -0
- package/dist/cli.js +4 -0
- package/dist/commands/setup.js +17 -7
- package/dist/store.d.ts +4 -0
- package/dist/store.js +35 -10
- package/dist/wire.d.ts +2 -2
- package/dist/wire.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,15 @@ already enrolled it connects this machine and writes its MCP configuration; when
|
|
|
41
41
|
leaves the checkout unchanged and Balladeer shows that enforcement tracking is unavailable until an
|
|
42
42
|
administrator connects a repository.
|
|
43
43
|
|
|
44
|
+
## If setup cannot save its local connection
|
|
45
|
+
|
|
46
|
+
Setup needs access to its private credential folder, normally `~/.config/balladeer`. If your
|
|
47
|
+
terminal or coding agent blocks that access, allow the command to access the private folder named in
|
|
48
|
+
the error, then repeat the same setup command with the same flags. Keep your existing saved
|
|
49
|
+
credentials; do not delete the store or move it into your repository. Setup checks local write
|
|
50
|
+
access before requesting a new browser pairing and reports a credential-store error separately from
|
|
51
|
+
a connection failure.
|
|
52
|
+
|
|
44
53
|
## Requirements
|
|
45
54
|
|
|
46
55
|
- Node 22 or newer. The command uses Node builtins and the pinned `@iarna/toml` parser to preserve
|
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,10 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
60
60
|
skipping quietly; --no-claude-desktop leaves that file alone entirely.
|
|
61
61
|
Quit and reopen the app afterwards: it reads its configuration at startup.
|
|
62
62
|
|
|
63
|
+
If setup cannot access its private credential folder, allow this command
|
|
64
|
+
to access the folder named in the error, then repeat the same setup command.
|
|
65
|
+
Keep existing saved credentials; do not delete the store to retry.
|
|
66
|
+
|
|
63
67
|
${CLI_INVOCATION} repositories [--json] [--control-plane <url>]
|
|
64
68
|
List the repositories this machine's GitHub account can see, marking the
|
|
65
69
|
one you are standing in and the ones Balladeer already has.
|
package/dist/commands/setup.js
CHANGED
|
@@ -15,7 +15,7 @@ import { findLegacyInstall, legacyMessage } from "../legacy.js";
|
|
|
15
15
|
import { formatInstant } from "../local-time.js";
|
|
16
16
|
import { checkoutEntryPath, commandLine, runningFromRegistryInstall } from "../release.js";
|
|
17
17
|
import { hostHint, repositoryHint } from "../repository.js";
|
|
18
|
-
import { StoreError, credentialsPath, dropPendingPairing, findPendingPairing, findSession, putAgent, putPendingPairing, putSession, readCredentials, writeCredentials, } from "../store.js";
|
|
18
|
+
import { StoreError, assertStoreWritable, credentialsPath, dropPendingPairing, findPendingPairing, findSession, putAgent, putPendingPairing, putSession, readCredentials, writeCredentials, } from "../store.js";
|
|
19
19
|
import { CLI_VERSION, DELEGATED_SCOPES, } from "../wire.js";
|
|
20
20
|
import { explainText } from "./explain.js";
|
|
21
21
|
import { probeAgentConnection } from "./mcp.js";
|
|
@@ -168,10 +168,11 @@ function pendingLines(pending, repository, host, waiting, approvalUri) {
|
|
|
168
168
|
...JOIN_OR_CREATE.split("\n").map((line) => (line.length === 0 ? "" : ` ${line}`)),
|
|
169
169
|
];
|
|
170
170
|
}
|
|
171
|
-
function storeSession(credentials, controlPlane, token, session) {
|
|
171
|
+
function storeSession(credentials, controlPlane, token, session, workspaceChoice) {
|
|
172
172
|
return putSession(dropPendingPairing(credentials, controlPlane), {
|
|
173
173
|
controlPlane,
|
|
174
174
|
sessionId: session.sessionId,
|
|
175
|
+
...(workspaceChoice?.startsWith("create:") ? { workspaceChoice } : {}),
|
|
175
176
|
token,
|
|
176
177
|
workspaceId: session.workspaceId,
|
|
177
178
|
workspaceName: session.workspaceName,
|
|
@@ -290,23 +291,29 @@ export async function runSetup(options) {
|
|
|
290
291
|
catch (error) {
|
|
291
292
|
return storeFailure(options, error);
|
|
292
293
|
}
|
|
293
|
-
//
|
|
294
|
-
//
|
|
294
|
+
// A completed create intent resumes only its own authenticated session. The
|
|
295
|
+
// name is local intent, never a lookup or grant of workspace authority. An
|
|
296
|
+
// explicit chooser always starts a switch. Keep the old session until the
|
|
297
|
+
// new pairing is claimed, so a failed start or claim loses no credentials.
|
|
295
298
|
const workspaceChoice = options.createWorkspace !== undefined
|
|
296
299
|
? `create:${carriedName}`
|
|
297
300
|
: options.chooseWorkspace === true
|
|
298
301
|
? "choose"
|
|
299
302
|
: undefined;
|
|
303
|
+
const previousSession = findSession(credentials, controlPlane);
|
|
304
|
+
const freshChoice = workspaceChoice !== undefined &&
|
|
305
|
+
(options.chooseWorkspace === true || previousSession?.workspaceChoice !== workspaceChoice);
|
|
300
306
|
if (workspaceChoice !== undefined) {
|
|
301
307
|
credentials = {
|
|
302
308
|
...credentials,
|
|
303
|
-
sessions: credentials.sessions.filter((entry) => entry.controlPlane !== controlPlane),
|
|
304
309
|
pendingPairings: credentials.pendingPairings.filter((entry) => entry.controlPlane !== controlPlane || entry.workspaceChoice === workspaceChoice),
|
|
305
310
|
};
|
|
311
|
+
}
|
|
312
|
+
if (freshChoice) {
|
|
306
313
|
say(options, "Choose the workspace in the new browser pairing. Existing agent connections remain saved.");
|
|
307
314
|
}
|
|
308
315
|
// A still-valid session carries straight on to the steps that need it.
|
|
309
|
-
const stored =
|
|
316
|
+
const stored = freshChoice ? undefined : previousSession;
|
|
310
317
|
if (stored && Date.parse(stored.expiresAt) > now().getTime()) {
|
|
311
318
|
try {
|
|
312
319
|
const answer = await request(controlPlane, {
|
|
@@ -378,6 +385,9 @@ export async function runSetup(options) {
|
|
|
378
385
|
}
|
|
379
386
|
let started;
|
|
380
387
|
try {
|
|
388
|
+
// An unsaved claim secret cannot be recovered by retrying the browser link.
|
|
389
|
+
// Check local access before creating an unapproved remote pairing.
|
|
390
|
+
assertStoreWritable(options.environment);
|
|
381
391
|
const { verifier, digest } = newVerifier();
|
|
382
392
|
started = await request(controlPlane, {
|
|
383
393
|
method: "POST",
|
|
@@ -450,7 +460,7 @@ async function claimOnce(options, credentials, pending) {
|
|
|
450
460
|
if (answer.status === "pending") {
|
|
451
461
|
return { kind: "pending", pollIntervalSeconds: answer.pollIntervalSeconds };
|
|
452
462
|
}
|
|
453
|
-
const updated = storeSession(credentials, options.controlPlane, answer.token, answer.session);
|
|
463
|
+
const updated = storeSession(credentials, options.controlPlane, answer.token, answer.session, pending.workspaceChoice);
|
|
454
464
|
writeCredentials(updated, options.environment);
|
|
455
465
|
reportPaired(options, answer.session);
|
|
456
466
|
const session = findSession(updated, options.controlPlane);
|
package/dist/store.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type PendingPairing = Readonly<{
|
|
|
15
15
|
workspaceChoice?: string;
|
|
16
16
|
}>;
|
|
17
17
|
export type StoredSession = Readonly<{
|
|
18
|
+
/** Claimed local create intent, not workspace identity or server authority. */
|
|
19
|
+
workspaceChoice?: string;
|
|
18
20
|
controlPlane: string;
|
|
19
21
|
sessionId: string;
|
|
20
22
|
token: string;
|
|
@@ -76,6 +78,8 @@ export declare function credentialsPath(environment?: NodeJS.ProcessEnv): string
|
|
|
76
78
|
*/
|
|
77
79
|
export declare function assertSafeStoreLocation(directory: string, environment?: NodeJS.ProcessEnv, cwd?: string): void;
|
|
78
80
|
export declare function ensureStoreDirectory(environment?: NodeJS.ProcessEnv): string;
|
|
81
|
+
/** Check actual write permission before requesting a pairing whose secret must be saved. */
|
|
82
|
+
export declare function assertStoreWritable(environment?: NodeJS.ProcessEnv): void;
|
|
79
83
|
export declare function readCredentials(environment?: NodeJS.ProcessEnv): Credentials;
|
|
80
84
|
/**
|
|
81
85
|
* Atomic, and never through a path an attacker could have pre-planted: the
|
package/dist/store.js
CHANGED
|
@@ -86,12 +86,38 @@ function assertPrivate(path, kind) {
|
|
|
86
86
|
throw new StoreError("credential_store_unsafe", `${path} is readable by other users. Run: chmod ${kind === "directory" ? "700" : "600"} ${path}`);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
function storeAccessError(error, path, operation) {
|
|
90
|
+
if (error instanceof StoreError)
|
|
91
|
+
return error;
|
|
92
|
+
const code = error?.code;
|
|
93
|
+
const permissionDenied = code === "EACCES" || code === "EPERM";
|
|
94
|
+
return new StoreError(operation === "read" ? "credential_store_unreadable" : "credential_store_unwritable", permissionDenied
|
|
95
|
+
? `Balladeer cannot ${operation} its local credential store at ${path}. Allow this command to access that private folder in your terminal or coding agent, then rerun the same command. Existing saved credentials were not replaced.`
|
|
96
|
+
: `Balladeer could not ${operation} its local credential store at ${path} (${code ?? "filesystem error"}). Check that this location is a private, accessible directory, then retry. Existing saved credentials were not replaced.`, { cause: error });
|
|
97
|
+
}
|
|
89
98
|
export function ensureStoreDirectory(environment = process.env) {
|
|
90
99
|
const directory = configHome(environment);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
try {
|
|
101
|
+
assertSafeStoreLocation(directory, environment);
|
|
102
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
103
|
+
assertPrivate(directory, "directory");
|
|
104
|
+
return directory;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw storeAccessError(error, directory, "write");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Check actual write permission before requesting a pairing whose secret must be saved. */
|
|
111
|
+
export function assertStoreWritable(environment = process.env) {
|
|
112
|
+
const probe = `.write-check-${randomBytes(8).toString("hex")}`;
|
|
113
|
+
const path = join(configHome(environment), probe);
|
|
114
|
+
try {
|
|
115
|
+
writeStoreFile(probe, "", environment);
|
|
116
|
+
unlinkSync(path);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw storeAccessError(error, configHome(environment), "write");
|
|
120
|
+
}
|
|
95
121
|
}
|
|
96
122
|
export function readCredentials(environment = process.env) {
|
|
97
123
|
const path = credentialsPath(environment);
|
|
@@ -101,9 +127,10 @@ export function readCredentials(environment = process.env) {
|
|
|
101
127
|
raw = readFileSync(path, "utf8");
|
|
102
128
|
}
|
|
103
129
|
catch (error) {
|
|
104
|
-
if (error
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
if (error?.code === "ENOENT") {
|
|
131
|
+
return { ...EMPTY, pendingPairings: [], sessions: [], agents: [] };
|
|
132
|
+
}
|
|
133
|
+
throw storeAccessError(error, path, "read");
|
|
107
134
|
}
|
|
108
135
|
try {
|
|
109
136
|
const parsed = JSON.parse(raw);
|
|
@@ -164,9 +191,7 @@ export function writeStoreFile(fileName, contents, environment = process.env) {
|
|
|
164
191
|
catch {
|
|
165
192
|
// The temporary file may never have been created.
|
|
166
193
|
}
|
|
167
|
-
|
|
168
|
-
throw error;
|
|
169
|
-
throw new StoreError("credential_store_unwritable", `Balladeer could not write ${path}: ${error instanceof Error ? error.message : "unknown reason"}`, { cause: error });
|
|
194
|
+
throw storeAccessError(error, path, "write");
|
|
170
195
|
}
|
|
171
196
|
}
|
|
172
197
|
/** Origins, not URL strings, so `https://host/../` cannot alias a stored entry. */
|
package/dist/wire.d.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export declare const CLI_VERSION = "1.0.
|
|
8
|
+
export declare const CLI_VERSION = "1.0.4";
|
|
9
9
|
export declare const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export declare const CLIENT_HEADER = "x-balladeer-client";
|
|
11
|
-
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.
|
|
11
|
+
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.4";
|
|
12
12
|
export declare const DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
13
13
|
export type DelegatedScope = "repository:enroll" | "agent:issue" | "ci:connect" | "workspace:invite" | "candidate:propose";
|
|
14
14
|
export declare const DELEGATED_SCOPES: readonly DelegatedScope[];
|
package/dist/wire.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export const CLI_VERSION = "1.0.
|
|
8
|
+
export const CLI_VERSION = "1.0.4";
|
|
9
9
|
export const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export const CLIENT_HEADER = "x-balladeer-client";
|
|
11
11
|
export const CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
|