borgmcp 2.0.1 → 2.0.3
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 +16 -10
- package/dist/claude.d.ts +1 -0
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +5 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +12 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/codex-app-wake.d.ts +9 -11
- package/dist/codex-app-wake.d.ts.map +1 -1
- package/dist/codex-app-wake.js +15 -11
- package/dist/codex-app-wake.js.map +1 -1
- package/dist/codex-wake-resolve.d.ts +4 -4
- package/dist/codex-wake-resolve.js +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -18
- package/dist/config.js.map +1 -1
- package/dist/credential-paths.d.ts +3 -0
- package/dist/credential-paths.d.ts.map +1 -0
- package/dist/credential-paths.js +7 -0
- package/dist/credential-paths.js.map +1 -0
- package/dist/log-stream.d.ts +4 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +58 -2
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts +25 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +59 -4
- package/dist/remote-client.js.map +1 -1
- package/dist/seat-store.d.ts +11 -5
- package/dist/seat-store.d.ts.map +1 -1
- package/dist/seat-store.js +147 -14
- package/dist/seat-store.js.map +1 -1
- package/dist/seats.d.ts +72 -0
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +119 -0
- package/dist/seats.js.map +1 -1
- package/dist/server-errors.d.ts +1 -1
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +50 -0
- package/dist/server-facade.d.ts.map +1 -0
- package/dist/server-facade.js +126 -0
- package/dist/server-facade.js.map +1 -0
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +49 -21
- package/dist/server-handshake.js.map +1 -1
- package/dist/session-continuity.d.ts +33 -0
- package/dist/session-continuity.d.ts.map +1 -0
- package/dist/session-continuity.js +220 -0
- package/dist/session-continuity.js.map +1 -0
- package/dist/token-store.d.ts +2 -1
- package/dist/token-store.d.ts.map +1 -1
- package/dist/token-store.js +5 -3
- package/dist/token-store.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +10 -7
- package/docs/LOCAL_SERVER.md +82 -43
- package/docs/RELEASING.md +22 -4
- package/package.json +2 -2
- package/src/claude.ts +5 -0
- package/src/cli-help.ts +15 -0
- package/src/codex-app-wake.ts +23 -11
- package/src/codex-wake-resolve.ts +4 -4
- package/src/config.ts +40 -18
- package/src/credential-paths.ts +8 -0
- package/src/log-stream.ts +64 -2
- package/src/remote-client.ts +92 -14
- package/src/seat-store.ts +159 -16
- package/src/seats.ts +170 -0
- package/src/server-errors.ts +3 -1
- package/src/server-facade.ts +192 -0
- package/src/server-handshake.ts +54 -21
- package/src/session-continuity.ts +280 -0
- package/src/token-store.ts +9 -4
- package/src/unknown-subcommand.ts +1 -0
package/docs/LOCAL_SERVER.md
CHANGED
|
@@ -4,52 +4,92 @@ Install and initialize the server in the human operator's terminal:
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g borgmcp-server
|
|
7
|
-
borg
|
|
8
|
-
borg
|
|
7
|
+
borg server setup
|
|
8
|
+
borg server start
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
The client owns the `borg server` facade. It forwards commands and renders
|
|
12
|
+
verified server evidence. It does not infer a checkout, activate an artifact,
|
|
13
|
+
create a service, or claim a build identity by itself. The server owns artifact
|
|
14
|
+
verification, activation, data and identity preservation, runtime build
|
|
15
|
+
identity, rollback, and explicit Linux/macOS service adapters. The server
|
|
16
|
+
executable remains the direct foreground authority.
|
|
17
|
+
|
|
18
|
+
`borg server start` and `borg-mcp-server start` are foreground commands. They
|
|
19
|
+
must never imply that a daemon, LaunchAgent, or systemd service was installed.
|
|
20
|
+
Ctrl-C stops the foreground process. Managed persistence is a separate explicit
|
|
21
|
+
handoff.
|
|
22
|
+
|
|
23
|
+
The available lifecycle facade commands are:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
Usage: borg server <command> [arguments]
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
setup Prepare local server identity and data; does not start the server.
|
|
30
|
+
start Start the verified server in the foreground.
|
|
31
|
+
status Report verified runtime evidence.
|
|
32
|
+
update Verify and activate a local server artifact.
|
|
33
|
+
invite Create a single-use invitation in an interactive terminal.
|
|
34
|
+
|
|
35
|
+
Run borg server <command> --help for server command options.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Local server client credentials are stored in the owner-controlled
|
|
39
|
+
`~/.borg/credentials` file with mode 0600. Fresh same-machine setup provisions
|
|
40
|
+
the first owner record there, so bare `borg assimilate` can use it without an
|
|
41
|
+
invitation prompt. Use `borg server invite` explicitly when another client or
|
|
42
|
+
device needs a single-use invitation; its output is owned by the server.
|
|
43
|
+
|
|
44
|
+
Status reports only runtime evidence supplied by the server: running/stopped
|
|
45
|
+
state, exact running artifact and immutable build identity when available,
|
|
46
|
+
endpoint, process mode, and data-identity availability. If the running build
|
|
47
|
+
identity is unavailable, status says it is unavailable. It never substitutes a
|
|
48
|
+
source checkout, package cache, or guessed version.
|
|
49
|
+
|
|
50
|
+
Update has four visible phases: verification, activation, result, and next
|
|
51
|
+
action. Only a verified artifact may activate. A verification failure says no
|
|
52
|
+
activation occurred and that the last verified runtime remains available.
|
|
53
|
+
|
|
54
|
+
Keep the foreground server running. Open a second operator terminal in the
|
|
55
|
+
project checkout and run:
|
|
13
56
|
|
|
14
57
|
```bash
|
|
15
|
-
borg assimilate
|
|
58
|
+
borg assimilate
|
|
16
59
|
```
|
|
17
60
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
versioned response is decoded and the authenticated protocol handshake succeeds.
|
|
32
|
-
A new process resumes that pending enrollment before displaying another
|
|
33
|
-
invitation prompt.
|
|
61
|
+
Setup provisions the first same-machine owner credential directly, so this flow
|
|
62
|
+
does not ask for an invitation. For another client or device, run `borg server
|
|
63
|
+
invite` in the server operator's interactive terminal. On the intended recipient,
|
|
64
|
+
run `borg assimilate --host <server> --enroll` and enter the single-use invitation
|
|
65
|
+
at the labeled hidden prompt. Never put an invitation in argv, environment
|
|
66
|
+
variables, logs, or diagnostics.
|
|
67
|
+
|
|
68
|
+
For explicit invitation enrollment, the client generates a 256-bit credential
|
|
69
|
+
and UUID retry key and persists the exact tuple as `PENDING` in the 0600
|
|
70
|
+
credential file before network I/O. An ambiguous exchange retries that tuple
|
|
71
|
+
exactly; the credential becomes active only after the versioned response is
|
|
72
|
+
decoded and the authenticated protocol handshake succeeds. A new process
|
|
73
|
+
resumes that pending enrollment before displaying another invitation prompt.
|
|
34
74
|
|
|
35
75
|
`--cube-name <name>` explicitly selects the repository cube name. Without an
|
|
36
76
|
explicit name, Borg uses the `origin` repository name or, when `origin` is
|
|
37
|
-
absent, proposes the sanitized repository-directory basename
|
|
38
|
-
|
|
39
|
-
repositories fail closed.
|
|
77
|
+
absent, proposes the sanitized repository-directory basename. Confirm
|
|
78
|
+
interactively or pass `--yes`; bare repositories fail closed.
|
|
40
79
|
|
|
41
80
|
The connection is HTTPS-only. Borg validates the server trust material, stores
|
|
42
|
-
enrollment
|
|
43
|
-
0600
|
|
44
|
-
with the active cube. Local requests
|
|
81
|
+
parent enrollment credentials in `~/.borg/credentials` and session credentials
|
|
82
|
+
in the existing 0600 seat store, and persists only an opaque credential
|
|
83
|
+
reference with the active cube. Local requests
|
|
45
84
|
use the server's `/api/cubes/*` coordination routes. They cannot use hosted OAuth
|
|
46
85
|
credentials or change authority implicitly.
|
|
47
86
|
|
|
48
|
-
The
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
The lifecycle facade invokes the separately installed `borgmcp-server`; it does
|
|
88
|
+
not bundle the server into the client. The server must be running and trusted
|
|
89
|
+
before assimilation. An owner enrollment carrying the persisted `create_cube`
|
|
90
|
+
capability creates one idempotent cube per repository during normal
|
|
91
|
+
assimilation, using the server-owned `default` role template; repeating an
|
|
92
|
+
ambiguous request does not duplicate the cube, and distinct repositories can
|
|
53
93
|
create distinct bounded cubes. An ordinary enrolled client is denied before a
|
|
54
94
|
create request is sent. Cloud-only capabilities fail explicitly rather than
|
|
55
95
|
being redirected.
|
|
@@ -73,20 +113,19 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
|
|
|
73
113
|
|
|
74
114
|
## Recovery commands
|
|
75
115
|
|
|
76
|
-
- No saved or rejected enrollment:
|
|
77
|
-
|
|
116
|
+
- No saved or rejected enrollment: generate a single-use invitation with
|
|
117
|
+
`borg server invite`, then run `borg assimilate --host <server> --enroll` from
|
|
118
|
+
the intended recipient's interactive terminal.
|
|
78
119
|
- Rejected or expired invitation: keep the server running and mint a replacement
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`borg assimilate --host <server> --enroll` with the replacement invitation.
|
|
120
|
+
invitation with `borg server invite`, then rerun `borg assimilate --host
|
|
121
|
+
<server> --enroll` with the replacement invitation.
|
|
82
122
|
- Rejected or unloadable local seat: run `borg reset-local-seat --host <server>`
|
|
83
|
-
to clear ONLY this worktree's saved local seat, then
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- Unreachable server: start or restart it with `borg-mcp-server start`, then
|
|
123
|
+
to clear ONLY this worktree's saved local seat, then rerun `borg assimilate
|
|
124
|
+
--host <server>` with the server still running.
|
|
125
|
+
- Unreachable server: start or restart it with `borg server start`, then
|
|
87
126
|
rerun `borg assimilate --host <server>`.
|
|
88
127
|
- Trust mismatch after an intentional server re-initialization: verify the
|
|
89
|
-
expected server identity, stop and restart `borg
|
|
128
|
+
expected server identity, stop and restart `borg server start`, then retry.
|
|
90
129
|
- Busy local seat store: wait for the other Borg process to finish, then rerun
|
|
91
130
|
the same command. If the local seat store cannot be read or written, ensure its
|
|
92
131
|
directory on this machine is readable and writable, then rerun.
|
|
@@ -96,7 +135,7 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
|
|
|
96
135
|
|
|
97
136
|
## Release status
|
|
98
137
|
|
|
99
|
-
This self-hosted path consumes the published `borgmcp-shared@0.4.
|
|
138
|
+
This self-hosted path consumes the published `borgmcp-shared@0.4.3` v2 registry
|
|
100
139
|
release. The matching server owner-enrollment, cube-create, attach, restart, log,
|
|
101
140
|
and SSE implementation must also pass the full process-level local dogfood gate.
|
|
102
141
|
Until that gate opens the self-hosted path remains preview-only, and the client
|
package/docs/RELEASING.md
CHANGED
|
@@ -4,15 +4,33 @@ The GitHub Actions workflow publishes one immutable, reviewed `borgmcp` version
|
|
|
4
4
|
from a protected annotated tag. The protected publish job uses npm Trusted
|
|
5
5
|
Publishing; no long-lived npm token is stored or exposed.
|
|
6
6
|
|
|
7
|
-
## Immutable
|
|
7
|
+
## Immutable Release Evidence
|
|
8
8
|
|
|
9
9
|
The existing lightweight `v2.0.0` tag points to
|
|
10
10
|
`90a078264f4d61c0140ad0a30357a4df42c34ab0`. Immutable workflow run
|
|
11
11
|
`29693915689` rejected it at annotated-tag verification and failed before package
|
|
12
12
|
creation or npm publication; the publish job was skipped and
|
|
13
13
|
`borgmcp@2.0.0` remains absent from npm. Never delete, move, replace, reuse, or
|
|
14
|
-
rerun that tag or run.
|
|
15
|
-
|
|
14
|
+
rerun that tag or run.
|
|
15
|
+
|
|
16
|
+
The annotated `v2.0.1` tag object
|
|
17
|
+
`def12ee40af665fc6c3af4873a7d566b3f844fc1` peels to protected-main commit
|
|
18
|
+
`b30fc54a4d73bda98db4630864cca796c8923dd9`. Workflow run `29748931957`
|
|
19
|
+
successfully published that exact source as `borgmcp@2.0.1`; the registry records
|
|
20
|
+
integrity
|
|
21
|
+
`sha512-Ah8IY2izZ774gYLKthRL9lfrV+JBk2o9HSlrWUplyZgoGqwVjVboHNon0hWWF5i/fObiCGikFOMY6qZ+vaeyCw==`.
|
|
22
|
+
Never move, replace, reuse, or rerun that tag or workflow. The next candidate
|
|
23
|
+
must use a fresh unused identity and requires the complete release gate again.
|
|
24
|
+
|
|
25
|
+
The annotated `v2.0.2` tag object
|
|
26
|
+
`a1fc1f8f05c3f1b4d7ccbef86e244955642165b5` peels to protected-main commit
|
|
27
|
+
`aced84956fdf41315904c8901e50a345f628152c`. Workflow run `29840120451`
|
|
28
|
+
successfully published that exact source as `borgmcp@2.0.2`; the registry records
|
|
29
|
+
integrity
|
|
30
|
+
`sha512-w0O0t/2wzeJpXEqb4jQGuqZeavEkfFKdz6poRT8q1TxDKHTlOlwo3auctE+X1kWU2s5JkSDcUiUdQdyLwu84IA==`.
|
|
31
|
+
Never move, replace, reuse, or rerun that tag or workflow. The next candidate
|
|
32
|
+
uses the unused `v2.0.3` identity from a fresh reviewed protected-main commit
|
|
33
|
+
and requires the complete release gate again.
|
|
16
34
|
|
|
17
35
|
## Release Prerequisites
|
|
18
36
|
|
|
@@ -23,7 +41,7 @@ Before creating the release tag, independently verify all of these conditions:
|
|
|
23
41
|
- the extraction review confirms no private backend secrets, deployment
|
|
24
42
|
configuration, customer data, local state, or duplicated shared contracts
|
|
25
43
|
entered the public package;
|
|
26
|
-
- the exact audited registry dependency `borgmcp-shared@0.4.
|
|
44
|
+
- the exact audited registry dependency `borgmcp-shared@0.4.3` remains locked to
|
|
27
45
|
its canonical tarball and integrity;
|
|
28
46
|
- the client and matching server pass the complete local dogfood gate;
|
|
29
47
|
- the selected stable client version is unused and the exact release commit is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
71
|
-
"borgmcp-shared": "0.4.
|
|
71
|
+
"borgmcp-shared": "0.4.3",
|
|
72
72
|
"chalk": "^5.3.0",
|
|
73
73
|
"prompts": "^2.4.2",
|
|
74
74
|
"which": "^4.0.0"
|
package/src/claude.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* fresh drone inside it (see spawn.ts)
|
|
15
15
|
* borg sync → Advance the current worktree across the 5
|
|
16
16
|
* lifecycle states (see sync.ts, gh#33)
|
|
17
|
+
* borg server <cmd> → Forward a lifecycle command to borg-mcp-server
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { spawn } from 'child_process';
|
|
@@ -78,8 +79,12 @@ import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
|
78
79
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
79
80
|
import { connectOpenCodeDrone, computeOpenCodePort, createOpenCodeLaunchKickoff, injectInitialKickoff } from './opencode-drone.js';
|
|
80
81
|
import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
82
|
+
import { runEarlyServerFacade } from './server-facade.js';
|
|
81
83
|
|
|
82
84
|
async function main() {
|
|
85
|
+
const serverExitCode = await runEarlyServerFacade(process.argv);
|
|
86
|
+
if (serverExitCode !== null) process.exit(serverExitCode);
|
|
87
|
+
|
|
83
88
|
// `--debug` / BORG_DEBUG: enable HTTP request/response logging to stderr
|
|
84
89
|
// (observability for failures like the cross-account assimilate 404).
|
|
85
90
|
// Done first so debug covers everything below; strips `--debug` from argv
|
package/src/cli-help.ts
CHANGED
|
@@ -36,12 +36,27 @@ export function topLevelHelpText(version: string): string {
|
|
|
36
36
|
` borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)\n` +
|
|
37
37
|
` borg launch-all [cube] --cli claude|codex|opencode\n` +
|
|
38
38
|
` Launch all drone worktrees with that agent CLI\n` +
|
|
39
|
+
` borg server <command> [arguments]\n` +
|
|
39
40
|
` borg --cli claude|codex|opencode Launch that agent CLI directly\n` +
|
|
40
41
|
` borg --version Show installed version\n\n` +
|
|
41
42
|
`All other arguments are passed through to the selected agent CLI.\n`
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** Product Design-approved client-owned copy for `borg server --help`. */
|
|
47
|
+
export function serverHelpText(): string {
|
|
48
|
+
return (
|
|
49
|
+
`Usage: borg server <command> [arguments]\n\n` +
|
|
50
|
+
`Commands:\n` +
|
|
51
|
+
` setup Prepare local server identity and data; does not start the server.\n` +
|
|
52
|
+
` start Start the verified server in the foreground.\n` +
|
|
53
|
+
` status Report verified runtime evidence.\n` +
|
|
54
|
+
` update Verify and activate a local server artifact.\n` +
|
|
55
|
+
` invite Create a single-use invitation in an interactive terminal.\n\n` +
|
|
56
|
+
`Run borg server <command> --help for server command options.\n`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
/**
|
|
46
61
|
* Help text for `borg assimilate --help` — the home for the full assimilate flag
|
|
47
62
|
* set. Model/provider configuration belongs to the selected agent CLI.
|
package/src/codex-app-wake.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getActiveCube,
|
|
3
|
+
getCodexWakeTarget,
|
|
4
|
+
setCodexWakeTarget,
|
|
5
|
+
type ActiveCube,
|
|
6
|
+
} from './cubes.js';
|
|
2
7
|
import { CodexAppServerClient } from './codex-app-server.js';
|
|
3
8
|
import { checkCodexBridgeHealthy } from './codex-remote.js';
|
|
9
|
+
import { hasPendingWakeActivity } from './remote-client.js';
|
|
4
10
|
import {
|
|
5
11
|
BORG_CODEX_REMOTE_WAKE_ENV,
|
|
6
12
|
resolveSessionAgentKind,
|
|
@@ -176,6 +182,10 @@ export interface CodexWakeDeps {
|
|
|
176
182
|
// WAKE_RETRY_MAX_ATTEMPTS); small values let tests prove the loop terminates
|
|
177
183
|
// under a non-advancing clock without running thousands of iterations.
|
|
178
184
|
maxAttempts?: number;
|
|
185
|
+
// client#76: token-free, non-mutating preflight for the periodic backstop.
|
|
186
|
+
// Per-entry and retry-drain paths do not use this: their pending obligation is
|
|
187
|
+
// already established by a concrete delivered/deferred event.
|
|
188
|
+
hasPendingWork?: (active: ActiveCube) => Promise<boolean>;
|
|
179
189
|
// gh#861 finding 2: lease-ownership gate for the heartbeat tick — a lease-LOSING
|
|
180
190
|
// duplicate child must NOT tick/inject (symmetry with the per-entry path, which
|
|
181
191
|
// only fires inside an SSE session that holds the stream lease). Heartbeat-only;
|
|
@@ -403,16 +413,13 @@ async function runRetryDrainLoop(deps: CodexWakeDeps): Promise<void> {
|
|
|
403
413
|
export const CODEX_HEARTBEAT_CADENCE_MS = 20 * 60_000;
|
|
404
414
|
|
|
405
415
|
/**
|
|
406
|
-
* gh#857
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* delivery
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* drain prompt is intentionally re-delivered each idle window. Best-effort: a
|
|
414
|
-
* mid-turn thread / transient error / unresolved target just skips this tick (the
|
|
415
|
-
* next tick retries). Never throws.
|
|
416
|
+
* gh#857/client#76: one tick of the codex catch-up backstop. The cadence only
|
|
417
|
+
* initiates a token-free unread-state preflight; a DRAIN turn is injected when
|
|
418
|
+
* that authoritative scan finds real work that a per-entry wake missed. Recent
|
|
419
|
+
* delivery still suppresses redundant preflights. Unlike the per-entry path it
|
|
420
|
+
* does not consult deliveredWakeKeys because the unread cursor is authoritative.
|
|
421
|
+
* Best-effort: a failed preflight, mid-turn thread, transient error, or unresolved
|
|
422
|
+
* target skips this tick and lets the next cadence retry. Never throws.
|
|
416
423
|
*/
|
|
417
424
|
export async function fireCodexHeartbeatTick(
|
|
418
425
|
deps: CodexWakeDeps = {},
|
|
@@ -434,6 +441,11 @@ export async function fireCodexHeartbeatTick(
|
|
|
434
441
|
try {
|
|
435
442
|
const active = await (deps.getActiveCube ?? getActiveCube)();
|
|
436
443
|
if (!active) return;
|
|
444
|
+
// Elapsed time alone must never cross the model boundary. Query the
|
|
445
|
+
// authoritative unread state without advancing its cursor; only then touch
|
|
446
|
+
// the app-server socket or resolve a thread.
|
|
447
|
+
const hasPendingWork = deps.hasPendingWork ?? hasPendingWakeActivity;
|
|
448
|
+
if (!(await hasPendingWork(active))) return;
|
|
437
449
|
const resolved = await resolveFreshCodexWakeTarget(active, deps);
|
|
438
450
|
if (!resolved) return; // thread not loaded yet → next tick retries
|
|
439
451
|
const client = makeCodexClient(resolved.socketPath, deps);
|
|
@@ -140,10 +140,10 @@ export function wakeRetryExpired(
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* WI-2 double-fire avoidance: the periodic
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
143
|
+
* WI-2 double-fire avoidance: the periodic catch-up preflight runs only when no
|
|
144
|
+
* wake delivery landed within the cadence window. A never-delivered seat (null)
|
|
145
|
+
* is eligible for preflight; client#76 separately requires pending unread work
|
|
146
|
+
* before a model turn can start.
|
|
147
147
|
*/
|
|
148
148
|
export function shouldFireHeartbeat(
|
|
149
149
|
lastDeliveredAt: number | null,
|
package/src/config.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Secure local-server credential storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* leaves the 0600 file, and a single store flock serializes every read-compare-write.
|
|
4
|
+
* Parent enrollment credentials and pending enrollment/cube-creation records
|
|
5
|
+
* rest ONLY in the canonical 0600 credential file. Per-seat session credentials
|
|
6
|
+
* remain in the separate seat store. A single flock serializes every parent-store
|
|
7
|
+
* read-compare-write shared by client enrollment and same-machine server setup.
|
|
9
8
|
*/
|
|
10
|
-
import os from 'os';
|
|
11
|
-
import path from 'path';
|
|
12
9
|
import { createHash, randomBytes, randomUUID } from 'crypto';
|
|
13
10
|
import type { ServerCapability } from 'borgmcp-shared/protocol';
|
|
14
11
|
import { withStoreLock } from './seat-store.js';
|
|
@@ -16,14 +13,15 @@ import {
|
|
|
16
13
|
makeFileBackend,
|
|
17
14
|
type TokenBackend,
|
|
18
15
|
} from './token-store.js';
|
|
16
|
+
import { BORG_USER_ROOT, SERVER_CREDENTIALS_FILE } from './credential-paths.js';
|
|
19
17
|
|
|
20
18
|
const SERVER_CREDENTIAL_RECORD_VERSION = 2 as const;
|
|
21
19
|
const SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 1 as const;
|
|
22
20
|
const SERVER_CUBE_RETRY_RECORD_VERSION = 1 as const;
|
|
23
21
|
// The 0600 credential store (Queen rescope: replaces the OS keychain). A single
|
|
24
|
-
// file holds every
|
|
22
|
+
// file holds every parent credential/enrollment record; a single flock
|
|
25
23
|
// serializes every mutator + observer that must (SR-seven #4).
|
|
26
|
-
const CREDENTIALS_FILE =
|
|
24
|
+
const CREDENTIALS_FILE = SERVER_CREDENTIALS_FILE;
|
|
27
25
|
const CREDENTIALS_LOCK = `${CREDENTIALS_FILE}.lock`;
|
|
28
26
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29
27
|
|
|
@@ -199,15 +197,39 @@ function serverCubeRetryAccount(
|
|
|
199
197
|
// parity with the server's TLS keys; no OS keychain, no obfuscation-grade
|
|
200
198
|
// fallback). The single store lock (CREDENTIALS_LOCK) serializes the RCW.
|
|
201
199
|
let serverCredentialBackendPromise: Promise<TokenBackend> | null = null;
|
|
200
|
+
let testBackendInjected = false;
|
|
201
|
+
let testLockTail: Promise<void> = Promise.resolve();
|
|
202
|
+
async function withCredentialStoreLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
203
|
+
if (!testBackendInjected) {
|
|
204
|
+
return withStoreLock(CREDENTIALS_LOCK, operation, {
|
|
205
|
+
secureRoot: BORG_USER_ROOT,
|
|
206
|
+
rootMode: 'owner-controlled',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const prior = testLockTail;
|
|
210
|
+
let release!: () => void;
|
|
211
|
+
testLockTail = new Promise<void>((resolveLock) => { release = resolveLock; });
|
|
212
|
+
await prior;
|
|
213
|
+
try {
|
|
214
|
+
return await operation();
|
|
215
|
+
} finally {
|
|
216
|
+
release();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
202
219
|
async function getServerCredentialBackend(): Promise<TokenBackend> {
|
|
203
220
|
if (!serverCredentialBackendPromise) {
|
|
204
|
-
serverCredentialBackendPromise = Promise.resolve(makeFileBackend(CREDENTIALS_FILE
|
|
221
|
+
serverCredentialBackendPromise = Promise.resolve(makeFileBackend(CREDENTIALS_FILE, {
|
|
222
|
+
secureRoot: BORG_USER_ROOT,
|
|
223
|
+
rootMode: 'owner-controlled',
|
|
224
|
+
}));
|
|
205
225
|
}
|
|
206
226
|
return serverCredentialBackendPromise;
|
|
207
227
|
}
|
|
208
228
|
|
|
209
229
|
/** Test-only credential-store backend injection. */
|
|
210
230
|
export function __setServerCredentialBackendForTest(backend: TokenBackend | null): void {
|
|
231
|
+
testBackendInjected = backend !== null;
|
|
232
|
+
testLockTail = Promise.resolve();
|
|
211
233
|
serverCredentialBackendPromise = backend ? Promise.resolve(backend) : null;
|
|
212
234
|
}
|
|
213
235
|
|
|
@@ -252,7 +274,7 @@ async function writeServerCredentialRecord(
|
|
|
252
274
|
*/
|
|
253
275
|
export async function storeServerCredential(record: ServerCredentialRecord): Promise<void> {
|
|
254
276
|
const backend = await getServerCredentialBackend();
|
|
255
|
-
await
|
|
277
|
+
await withCredentialStoreLock(() => writeServerCredentialRecord(backend, record));
|
|
256
278
|
}
|
|
257
279
|
|
|
258
280
|
/** Read an authority-bound active client record, failing closed on corruption. */
|
|
@@ -346,7 +368,7 @@ export async function getPendingServerEnrollment(
|
|
|
346
368
|
validateServerCredentialBinding(origin, trustIdentity);
|
|
347
369
|
const backend = await getServerCredentialBackend();
|
|
348
370
|
const account = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
349
|
-
return
|
|
371
|
+
return withCredentialStoreLock(async () => {
|
|
350
372
|
const stored = await backend.get(account);
|
|
351
373
|
if (!stored) return null;
|
|
352
374
|
try {
|
|
@@ -375,7 +397,7 @@ export async function getOrCreatePendingServerEnrollment(
|
|
|
375
397
|
validateClientName(input.clientName);
|
|
376
398
|
const backend = await getServerCredentialBackend();
|
|
377
399
|
const account = serverPendingEnrollmentAccount(input.origin, input.trustIdentity);
|
|
378
|
-
return
|
|
400
|
+
return withCredentialStoreLock(async () => {
|
|
379
401
|
const stored = await backend.get(account);
|
|
380
402
|
if (stored) {
|
|
381
403
|
try {
|
|
@@ -432,7 +454,7 @@ export async function activatePendingServerEnrollment(
|
|
|
432
454
|
const serverCapabilities = validateServerCapabilities(input.serverCapabilities);
|
|
433
455
|
const backend = await getServerCredentialBackend();
|
|
434
456
|
const pendingAccount = serverPendingEnrollmentAccount(input.origin, input.trustIdentity);
|
|
435
|
-
await
|
|
457
|
+
await withCredentialStoreLock(async () => {
|
|
436
458
|
const stored = await backend.get(pendingAccount);
|
|
437
459
|
if (!stored) throw new Error('pending Borg server enrollment is missing');
|
|
438
460
|
try {
|
|
@@ -469,7 +491,7 @@ export async function clearPendingServerEnrollment(
|
|
|
469
491
|
validateUuid(retryKey, 'enrollment retry key');
|
|
470
492
|
const backend = await getServerCredentialBackend();
|
|
471
493
|
const account = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
472
|
-
await
|
|
494
|
+
await withCredentialStoreLock(async () => {
|
|
473
495
|
const stored = await backend.get(account);
|
|
474
496
|
if (!stored) return;
|
|
475
497
|
try {
|
|
@@ -510,7 +532,7 @@ export async function getOrCreatePendingServerCubeCreation(
|
|
|
510
532
|
input.clientId,
|
|
511
533
|
repositoryBinding,
|
|
512
534
|
);
|
|
513
|
-
return
|
|
535
|
+
return withCredentialStoreLock(async () => {
|
|
514
536
|
const stored = await backend.get(account);
|
|
515
537
|
if (stored) {
|
|
516
538
|
try {
|
|
@@ -572,7 +594,7 @@ export async function clearPendingServerCubeCreation(
|
|
|
572
594
|
record.clientId,
|
|
573
595
|
record.repositoryBinding,
|
|
574
596
|
);
|
|
575
|
-
await
|
|
597
|
+
await withCredentialStoreLock(async () => {
|
|
576
598
|
const stored = await backend.get(account);
|
|
577
599
|
if (!stored) return;
|
|
578
600
|
try {
|
|
@@ -588,7 +610,7 @@ export async function clearPendingServerCubeCreation(
|
|
|
588
610
|
export async function clearServerCredential(origin: string, trustIdentity: string): Promise<void> {
|
|
589
611
|
const backend = await getServerCredentialBackend();
|
|
590
612
|
const pendingAccount = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
591
|
-
await
|
|
613
|
+
await withCredentialStoreLock(async () => {
|
|
592
614
|
await backend.delete(serverCredentialAccount(origin, trustIdentity));
|
|
593
615
|
await backend.delete(pendingAccount);
|
|
594
616
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const canonicalHome = realpathSync(homedir());
|
|
6
|
+
|
|
7
|
+
export const BORG_USER_ROOT = join(canonicalHome, '.borg');
|
|
8
|
+
export const SERVER_CREDENTIALS_FILE = join(BORG_USER_ROOT, 'credentials');
|