agent-dag 3.22.0 → 3.22.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 +6 -477
- package/package.json +14 -48
- package/shim.js +107 -0
- package/LICENSE +0 -661
- package/LICENSING.md +0 -82
- package/THIRD_PARTY_NOTICES.md +0 -395
- package/bin/agent-dag.js +0 -626
- package/bin/deck.js +0 -1805
- package/dist/web/assets/index-3FWd7g_W.css +0 -1
- package/dist/web/assets/index-BOwtoP02.js +0 -266
- package/dist/web/index.html +0 -49
- package/hook/hook.js +0 -542
- package/release-notes.json +0 -392
- package/src/server/activity.mjs +0 -52
- package/src/server/agent-activity.mjs +0 -522
- package/src/server/args.mjs +0 -183
- package/src/server/auto-update.mjs +0 -79
- package/src/server/block-notify.mjs +0 -173
- package/src/server/boot-deadline.mjs +0 -127
- package/src/server/brand.mjs +0 -16
- package/src/server/browser-history.mjs +0 -497
- package/src/server/browser-presence.mjs +0 -211
- package/src/server/browser-profiles.mjs +0 -279
- package/src/server/browser-react.mjs +0 -284
- package/src/server/browser-watch-store.mjs +0 -350
- package/src/server/browser-watch.mjs +0 -905
- package/src/server/ccusage.mjs +0 -1168
- package/src/server/claude-accounts.mjs +0 -951
- package/src/server/claude-dir.mjs +0 -213
- package/src/server/codex-auth.mjs +0 -388
- package/src/server/codex-dir.mjs +0 -171
- package/src/server/codex-quota.mjs +0 -449
- package/src/server/codex-usage.mjs +0 -512
- package/src/server/cswap-admin.mjs +0 -1562
- package/src/server/cswap-auto.mjs +0 -658
- package/src/server/cswap-install.mjs +0 -641
- package/src/server/deck-home.mjs +0 -243
- package/src/server/deck-prefs.mjs +0 -301
- package/src/server/deck-probe.mjs +0 -111
- package/src/server/detach.mjs +0 -244
- package/src/server/exec.mjs +0 -996
- package/src/server/global-install.mjs +0 -67
- package/src/server/hwmonitor.mjs +0 -56
- package/src/server/index.mjs +0 -6043
- package/src/server/installer.mjs +0 -912
- package/src/server/invoked-as.mjs +0 -144
- package/src/server/lan-about.mjs +0 -119
- package/src/server/lan-engine.mjs +0 -952
- package/src/server/lan-reach.mjs +0 -256
- package/src/server/lan-socket.mjs +0 -682
- package/src/server/lan-sync.mjs +0 -941
- package/src/server/lhm-parse.mjs +0 -91
- package/src/server/log-tail.mjs +0 -139
- package/src/server/log-writer.mjs +0 -322
- package/src/server/login-service.mjs +0 -473
- package/src/server/macmon.mjs +0 -310
- package/src/server/npx.mjs +0 -264
- package/src/server/open-url.mjs +0 -242
- package/src/server/presence.mjs +0 -40
- package/src/server/quota.mjs +0 -792
- package/src/server/relay-guard.mjs +0 -507
- package/src/server/reset-label.mjs +0 -78
- package/src/server/retire-sound-hook.mjs +0 -349
- package/src/server/running-deck.mjs +0 -234
- package/src/server/self-update.mjs +0 -1380
- package/src/server/stop-deck.mjs +0 -171
- package/src/server/supervisor.mjs +0 -392
- package/src/server/system-metrics.mjs +0 -1825
- package/src/server/term.mjs +0 -686
- package/src/server/uv-bootstrap.mjs +0 -337
package/src/server/lan-reach.mjs
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
// Why another deck cannot reach this one — read off the machine, not guessed.
|
|
2
|
-
//
|
|
3
|
-
// THE FAILURE THIS EXISTS FOR. Two decks on one router, LAN sync on at both
|
|
4
|
-
// ends, and one of them shows an empty list forever. Reported as "he sees me
|
|
5
|
-
// and I see nobody", which is the shape that gives it away: Windows lets a
|
|
6
|
-
// process send without asking anybody, and drops unsolicited inbound unless a
|
|
7
|
-
// rule says otherwise. So the beacon leaves, the far deck hears it and draws a
|
|
8
|
-
// row — and the answer coming back is dropped at this machine's edge. Both
|
|
9
|
-
// halves of the feature look broken from here and neither is.
|
|
10
|
-
//
|
|
11
|
-
// WHY IT CANNOT BE FIXED FROM HERE, and this is settled rather than open.
|
|
12
|
-
// Creating a firewall rule needs elevation, and `relay-guard.mjs` already wrote
|
|
13
|
-
// down why no route in this server may raise a password dialog: `index.mjs`
|
|
14
|
-
// deliberately trusts a request carrying no Origin header so hook.js and curl
|
|
15
|
-
// keep working, on the reasoning that a process able to POST to loopback can
|
|
16
|
-
// already run anything as the user. That holds only while no route can do
|
|
17
|
-
// something the caller could not do for itself. One route that elevates hands
|
|
18
|
-
// every local process a way to make an authentication prompt appear wearing
|
|
19
|
-
// ccdeck's name, at a moment ccdeck chose. So this module reads, decides, and
|
|
20
|
-
// hands back a command as TEXT. It is the same shape and the same refusal.
|
|
21
|
-
//
|
|
22
|
-
// WHY IT IS WORTH SAYING ANYTHING AT ALL, given a manual address already makes
|
|
23
|
-
// the feature work without any of this: because "nothing here yet" and "this
|
|
24
|
-
// machine cannot be reached" are different facts, and an empty list says the
|
|
25
|
-
// first while meaning the second. A person reading it concludes the feature is
|
|
26
|
-
// broken, or that the other machine is off. Neither is true and neither is
|
|
27
|
-
// actionable.
|
|
28
|
-
//
|
|
29
|
-
// EVERYTHING BELOW IS A PURE FUNCTION. The probe is one PowerShell call that
|
|
30
|
-
// knows nothing and reports three lists; every judgement is here, where the
|
|
31
|
-
// suite can run it on any platform. That is the same division lan-sync.mjs and
|
|
32
|
-
// lan-socket.mjs already keep, for the same reason.
|
|
33
|
-
|
|
34
|
-
/** What the probe asks Windows for. Three reads, none of them privileged —
|
|
35
|
-
* measured: `Get-NetConnectionProfile`, `Get-NetFirewallProfile` and
|
|
36
|
-
* `Get-NetFirewallApplicationFilter` all answer for an ordinary user, while
|
|
37
|
-
* `Get-NetFirewallPortFilter` returns "Access is denied". So the rule this
|
|
38
|
-
* looks for is scoped to a PROGRAM rather than to a port, which is also the
|
|
39
|
-
* only kind that keeps working: the sync listener's port is ephemeral and is
|
|
40
|
-
* a different number after every restart. */
|
|
41
|
-
export const PROBE_PS = [
|
|
42
|
-
"$ErrorActionPreference='SilentlyContinue'",
|
|
43
|
-
"$nets = @(Get-NetConnectionProfile | ForEach-Object { [pscustomobject]@{ alias=$_.InterfaceAlias; category=[string]$_.NetworkCategory; v4=[string]$_.IPv4Connectivity } })",
|
|
44
|
-
"$profiles = @(Get-NetFirewallProfile | ForEach-Object { [pscustomobject]@{ name=[string]$_.Name; enabled=[bool]$_.Enabled } })",
|
|
45
|
-
"$rules = @(Get-NetFirewallApplicationFilter | Where-Object { $_.Program -eq $env:CCDECK_EXE } | ForEach-Object { $_ | Get-NetFirewallRule } | ForEach-Object { [pscustomobject]@{ direction=[string]$_.Direction; action=[string]$_.Action; enabled=[bool]$_.Enabled; profile=[string]$_.Profile } })",
|
|
46
|
-
// WHICH INTERFACE IS THE LAN, asked of the routing table rather than guessed
|
|
47
|
-
// from a name. The beacon goes to 255.255.255.255, so the interface that
|
|
48
|
-
// carries the broadcast IS the one this feature lives or dies on — and on a
|
|
49
|
-
// machine with Tailscale beside wifi the two have different categories, so
|
|
50
|
-
// picking the wrong one reports Private and clears a machine that is blocked.
|
|
51
|
-
"$bcast = [string](Find-NetRoute -RemoteIPAddress 255.255.255.255 | Select-Object -First 1 -ExpandProperty InterfaceAlias)",
|
|
52
|
-
"[pscustomobject]@{ nets=$nets; profiles=$profiles; rules=$rules; bcast=$bcast } | ConvertTo-Json -Depth 4 -Compress",
|
|
53
|
-
].join("; ");
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Read what the probe printed, or null.
|
|
57
|
-
*
|
|
58
|
-
* REFUSES RATHER THAN GUESSES. Every field here comes back from a shell on
|
|
59
|
-
* somebody else's machine, where a policy, a missing cmdlet or a locale can
|
|
60
|
-
* turn any of it into something this did not expect. A null answer means the
|
|
61
|
-
* panel says nothing, which is what it did before this module existed — the
|
|
62
|
-
* one outcome that cannot make anything worse.
|
|
63
|
-
*
|
|
64
|
-
* `ConvertTo-Json` collapses a one-element array to a bare object, which is
|
|
65
|
-
* the classic way a Windows probe passes its own tests and fails on the one
|
|
66
|
-
* machine that has exactly one network. Normalised here rather than worked
|
|
67
|
-
* around at each use.
|
|
68
|
-
*/
|
|
69
|
-
export function readProbe(stdout) {
|
|
70
|
-
let raw = null;
|
|
71
|
-
try { raw = JSON.parse(String(stdout ?? "").trim()); }
|
|
72
|
-
catch { return null; }
|
|
73
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
74
|
-
const list = v => (Array.isArray(v) ? v : v == null ? [] : [v]).filter(x => x && typeof x === "object");
|
|
75
|
-
return {
|
|
76
|
-
nets: list(raw.nets).map(n => ({
|
|
77
|
-
alias: typeof n.alias === "string" ? n.alias : "",
|
|
78
|
-
category: typeof n.category === "string" ? n.category : "",
|
|
79
|
-
v4: typeof n.v4 === "string" ? n.v4 : "",
|
|
80
|
-
})).filter(n => n.alias),
|
|
81
|
-
profiles: list(raw.profiles).map(p => ({
|
|
82
|
-
name: typeof p.name === "string" ? p.name : "",
|
|
83
|
-
enabled: p.enabled === true,
|
|
84
|
-
})).filter(p => p.name),
|
|
85
|
-
rules: list(raw.rules).map(r => ({
|
|
86
|
-
direction: typeof r.direction === "string" ? r.direction : "",
|
|
87
|
-
action: typeof r.action === "string" ? r.action : "",
|
|
88
|
-
enabled: r.enabled === true,
|
|
89
|
-
profile: typeof r.profile === "string" ? r.profile : "",
|
|
90
|
-
})),
|
|
91
|
-
bcast: typeof raw.bcast === "string" ? raw.bcast.trim() : "",
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** The interfaces this machine holds a routable v4 address on, as the names
|
|
96
|
-
* Windows knows them by. `faces` is passed in rather than read, for the reason
|
|
97
|
-
* everything else here is pure. Measured on Windows 11 26200: node's key and
|
|
98
|
-
* `Get-NetConnectionProfile`'s `InterfaceAlias` are the same string, which is
|
|
99
|
-
* what makes matching them worth doing at all. */
|
|
100
|
-
export function localAliases(faces) {
|
|
101
|
-
const out = [];
|
|
102
|
-
for (const [name, list] of Object.entries(faces ?? {})) {
|
|
103
|
-
for (const n of list ?? []) {
|
|
104
|
-
if (n?.internal) continue;
|
|
105
|
-
if (n?.family !== "IPv4" && n?.family !== 4) continue;
|
|
106
|
-
if (typeof n.address !== "string" || n.address.startsWith("169.254.")) continue;
|
|
107
|
-
if (!out.includes(name)) out.push(name);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** A connection profile's category is spelled `DomainAuthenticated`; the
|
|
114
|
-
* firewall profile it answers to is spelled `Domain`. One mapping, in one
|
|
115
|
-
* place, because getting it wrong reads the wrong profile's `Enabled` and
|
|
116
|
-
* produces a confident wrong answer rather than a visible failure. */
|
|
117
|
-
export function profileFor(category) {
|
|
118
|
-
const c = String(category ?? "").trim().toLowerCase();
|
|
119
|
-
if (c === "public") return "Public";
|
|
120
|
-
if (c === "private") return "Private";
|
|
121
|
-
if (c === "domainauthenticated" || c === "domain") return "Domain";
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* The network this deck is actually on.
|
|
127
|
-
*
|
|
128
|
-
* A machine with Tailscale, a container bridge and wifi has several profiles
|
|
129
|
-
* and only one of them is the LAN somebody means.
|
|
130
|
-
*
|
|
131
|
-
* `aliases` IS IN PREFERENCE ORDER and is walked in that order rather than
|
|
132
|
-
* matched as a set. The caller puts the broadcast route's interface first,
|
|
133
|
-
* because that is the one the beacon actually leaves by; matching whichever
|
|
134
|
-
* happened to come first in the probe's own output is how the machine this was
|
|
135
|
-
* written on reports Tailscale's Private and clears a wifi that is Public.
|
|
136
|
-
*/
|
|
137
|
-
export function lanNet(nets, aliases = []) {
|
|
138
|
-
const rows = Array.isArray(nets) ? nets : [];
|
|
139
|
-
for (const a of (Array.isArray(aliases) ? aliases : [])) {
|
|
140
|
-
const want = String(a ?? "").trim().toLowerCase();
|
|
141
|
-
if (!want) continue;
|
|
142
|
-
const hit = rows.find(n => n.alias.toLowerCase() === want);
|
|
143
|
-
if (hit) return hit;
|
|
144
|
-
}
|
|
145
|
-
return rows.find(n => n.v4 && n.v4 !== "Disconnected") ?? rows[0] ?? null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Does an inbound allow rule for this program already cover that profile?
|
|
149
|
-
* A rule's `Profile` is a comma-joined list, and `Any` means all of them. */
|
|
150
|
-
export function ruleCovers(rules, profileName) {
|
|
151
|
-
const want = String(profileName ?? "").toLowerCase();
|
|
152
|
-
return (Array.isArray(rules) ? rules : []).some(r => {
|
|
153
|
-
if (!r.enabled) return false;
|
|
154
|
-
if (r.direction.toLowerCase() !== "inbound") return false;
|
|
155
|
-
if (r.action.toLowerCase() !== "allow") return false;
|
|
156
|
-
const on = r.profile.split(",").map(s => s.trim().toLowerCase());
|
|
157
|
-
return on.includes("any") || on.includes(want);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* The command that would change it — as text, for a person to paste into an
|
|
163
|
-
* elevated shell. Never run from here; see the header.
|
|
164
|
-
*
|
|
165
|
-
* ONE RULE, SCOPED TO THE PROGRAM. It covers the discovery socket and the sync
|
|
166
|
-
* listener at once, and it goes on covering the sync listener after a restart
|
|
167
|
-
* moves it — a rule written against today's port is a rule that works today.
|
|
168
|
-
*
|
|
169
|
-
* The category line is only offered when the network is Public, and it is
|
|
170
|
-
* offered rather than assumed: Public is the right setting for a café and the
|
|
171
|
-
* wrong one for the router at home, and this module cannot tell which one
|
|
172
|
-
* somebody is sitting in. The panel says so beside it.
|
|
173
|
-
*/
|
|
174
|
-
export function fixSteps({ category, alias, exePath }) {
|
|
175
|
-
const steps = [];
|
|
176
|
-
if (profileFor(category) === "Public" && alias) {
|
|
177
|
-
steps.push(`Set-NetConnectionProfile -InterfaceAlias "${alias}" -NetworkCategory Private`);
|
|
178
|
-
}
|
|
179
|
-
steps.push(
|
|
180
|
-
"New-NetFirewallRule -DisplayName \"ccdeck (local network)\" -Direction Inbound"
|
|
181
|
-
+ ` -Program "${exePath}" -Action Allow -Profile Private`,
|
|
182
|
-
);
|
|
183
|
-
return steps;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Whether other decks can reach this one, and what to do when they cannot.
|
|
188
|
-
*
|
|
189
|
-
* `null` means "no opinion", and it is the answer for every machine this cannot
|
|
190
|
-
* speak about: anything that is not Windows, and any probe that did not come
|
|
191
|
-
* back. Silence rather than a hedge — a panel line that says "possibly" about a
|
|
192
|
-
* thing it did not measure is worse than no line.
|
|
193
|
-
*
|
|
194
|
-
* `blocked: false` is a real finding and is worth returning: it lets the panel
|
|
195
|
-
* stop blaming the firewall for an empty list and say the other thing instead,
|
|
196
|
-
* which is that nobody else is running.
|
|
197
|
-
*/
|
|
198
|
-
export function reachability({ platform, probe, aliases = [], exePath = "" } = {}) {
|
|
199
|
-
if (platform !== "win32") return null;
|
|
200
|
-
if (!probe) return null;
|
|
201
|
-
// The broadcast route first, then the interfaces the deck holds an address
|
|
202
|
-
// on. The routing table is the authority on where a beacon goes; the deck's
|
|
203
|
-
// own addresses are the fallback for a machine where that read came back
|
|
204
|
-
// empty, and the probe's first connected network is the fallback for that.
|
|
205
|
-
const net = lanNet(probe.nets, [probe.bcast, ...aliases]);
|
|
206
|
-
const category = net?.category ?? "";
|
|
207
|
-
const name = profileFor(category);
|
|
208
|
-
if (!name) return null;
|
|
209
|
-
const prof = probe.profiles.find(p => p.name.toLowerCase() === name.toLowerCase());
|
|
210
|
-
// A firewall that is off blocks nothing, and saying otherwise would send
|
|
211
|
-
// somebody to add a rule that changes nothing.
|
|
212
|
-
if (prof && !prof.enabled) return { blocked: false, why: "firewall off", category, alias: net?.alias ?? "" };
|
|
213
|
-
if (ruleCovers(probe.rules, name)) {
|
|
214
|
-
return { blocked: false, why: "rule present", category, alias: net?.alias ?? "" };
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
blocked: true,
|
|
218
|
-
why: "no inbound rule",
|
|
219
|
-
category,
|
|
220
|
-
alias: net?.alias ?? "",
|
|
221
|
-
// The reason in the reader's terms, not the registry's. What they need to
|
|
222
|
-
// know is which half is broken, because the other half is what makes the
|
|
223
|
-
// workaround below obvious rather than magic.
|
|
224
|
-
text: name === "Public"
|
|
225
|
-
? "This network is set to Public, and Windows drops what other decks send. They can still hear this deck — that is why one of them may already show it."
|
|
226
|
-
: "Windows has no inbound rule for this deck, so it drops what other decks send. They can still hear it — that is why one of them may already show this machine.",
|
|
227
|
-
steps: fixSteps({ category, alias: net?.alias ?? "", exePath }),
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* The way out that needs no rule at all, and the reason this module is not the
|
|
233
|
-
* whole answer.
|
|
234
|
-
*
|
|
235
|
-
* A round is one OUTBOUND connection — `roundWith` dials, both sides prove
|
|
236
|
-
* themselves over that socket, and the manifest and any transfer ride it home.
|
|
237
|
-
* Nothing in a round needs this machine to accept a connection. So a deck that
|
|
238
|
-
* cannot be reached can still do every part of this by dialling first, and the
|
|
239
|
-
* two controls that make it dial are an address somebody typed and an invite
|
|
240
|
-
* somebody else minted.
|
|
241
|
-
*
|
|
242
|
-
* WHICH WAY THE INVITE GOES IS THE WHOLE OF IT, and nothing said so before.
|
|
243
|
-
* An invite carries the addresses of the deck that MINTED it, and the deck that
|
|
244
|
-
* PASTES it dials them. So a blocked machine that mints an invite is asking to
|
|
245
|
-
* be called, which is the one thing it cannot receive; the same pair works on
|
|
246
|
-
* the first try if the invite is minted at the other end and pasted here. Same
|
|
247
|
-
* two controls, opposite order, and only one of the orders works.
|
|
248
|
-
*/
|
|
249
|
-
export function workaround(blocked) {
|
|
250
|
-
if (!blocked) return null;
|
|
251
|
-
return {
|
|
252
|
-
dial: "This deck can still reach out, and one connection is all a round needs.",
|
|
253
|
-
invite: "Ask them to make the invite and paste it here — an invite is dialled by whoever pastes it, so it has to be pasted on this machine rather than made on it.",
|
|
254
|
-
address: "Or type their address below, and this deck will call them.",
|
|
255
|
-
};
|
|
256
|
-
}
|