@scopebond/agent 0.2.1 → 0.3.1
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 +15 -2
- package/dist/health.d.ts +18 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +22 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/override-reasons.d.ts +7 -0
- package/dist/override-reasons.d.ts.map +1 -0
- package/dist/override-reasons.js +56 -0
- package/dist/override-reasons.js.map +1 -0
- package/dist/prompt.d.ts +25 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +160 -0
- package/dist/prompt.js.map +1 -0
- package/dist/selfcheck.d.ts.map +1 -1
- package/dist/selfcheck.js +6 -0
- package/dist/selfcheck.js.map +1 -1
- package/dist/service.d.ts +5 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +46 -3
- package/dist/service.js.map +1 -1
- package/dist/tray.d.ts +9 -0
- package/dist/tray.d.ts.map +1 -0
- package/dist/tray.js +104 -0
- package/dist/tray.js.map +1 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -17,6 +17,11 @@ agent never takes part in a decision. It only keeps everything around the decisi
|
|
|
17
17
|
- **Self-check.** Once a day it checks this computer's side (hook entry present and able to start, autostart on,
|
|
18
18
|
nothing stuck in the queue, connection not about to lapse) and sends the result, signed with the key the computer
|
|
19
19
|
enrolled with, so the workspace can show that the whole path works end to end, or what broke.
|
|
20
|
+
- **Overrides.** When the workspace sets a rule to *Block, user may override*, the hook asks the agent, and the agent
|
|
21
|
+
shows its own window (Windows PowerShell with Windows Forms, macOS `osascript`, Linux `zenity`): the rule, the action and a
|
|
22
|
+
reason field. Only the window answers; whatever calls the local channel can open a window but never decide it. One window
|
|
23
|
+
at a time; it closes itself before the hook stops waiting. The reason goes to the workspace once, and waits in the
|
|
24
|
+
Scopebond home while the computer is offline.
|
|
20
25
|
- **Health.** It reports the same machine-readable status as `scopebond status --json` (`scopebond.status.v1`).
|
|
21
26
|
|
|
22
27
|
## Use
|
|
@@ -41,7 +46,7 @@ On Windows, type `npx.cmd` instead of `npx` in PowerShell if you run it without
|
|
|
41
46
|
## Local control channel
|
|
42
47
|
|
|
43
48
|
The agent listens on `127.0.0.1` only, on a random port, and every request must carry a random token. Port, token and
|
|
44
|
-
process id are in `~/.scopebond/agent.json`, readable by you only. Routes: `GET /status`, `POST /flush`, `POST /repair`, `POST /maintain`.
|
|
49
|
+
process id are in `~/.scopebond/agent.json`, readable by you only. Routes: `GET /status`, `POST /flush`, `POST /repair`, `POST /maintain`, `POST /override`.
|
|
45
50
|
Exactly one agent serves a computer's Scopebond home; a second one exits.
|
|
46
51
|
|
|
47
52
|
## Autostart
|
|
@@ -51,6 +56,14 @@ runs (the paths recorded at setup first, then the ones on the system), so a Node
|
|
|
51
56
|
leaves autostart pointing at nothing. On Windows the Run entry starts it under `conhost --headless`, so no window
|
|
52
57
|
opens at sign-in. `status` says whether autostart is on and working.
|
|
53
58
|
|
|
59
|
+
## Where you see it
|
|
60
|
+
|
|
61
|
+
On Windows the agent shows a tray icon (from Windows' own PowerShell, nothing extra to install): a green, amber or red
|
|
62
|
+
dot, what it means as its tooltip, and a menu with the one fix (Repair, Send now or Check again). It warns with a balloon
|
|
63
|
+
when it turns amber or red, and closes when the agent stops. Set `SCOPEBOND_AGENT_TRAY=off` for none. On macOS and Linux
|
|
64
|
+
the agent sends a system notification when things get worse, and once more when they recover. `GET /status` carries the
|
|
65
|
+
same `health` (level, headline, fix).
|
|
66
|
+
|
|
54
67
|
## Status
|
|
55
68
|
|
|
56
|
-
Experimental.
|
|
69
|
+
Experimental.
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StatusJson } from "@scopebond/hook";
|
|
2
|
+
export type HealthLevel = "green" | "amber" | "red";
|
|
3
|
+
export interface Health {
|
|
4
|
+
level: HealthLevel;
|
|
5
|
+
headline: string;
|
|
6
|
+
/** The local-channel route that fixes it, with its button label, when one exists. */
|
|
7
|
+
fix: {
|
|
8
|
+
label: string;
|
|
9
|
+
route: "/repair" | "/flush" | "/maintain";
|
|
10
|
+
} | null;
|
|
11
|
+
/** What a person types when no button can fix it. */
|
|
12
|
+
hint: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function healthOf(status: StatusJson, selfCheck: {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
failed: string[];
|
|
17
|
+
} | null | undefined): Health;
|
|
18
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;AACpD,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,qDAAqD;IACrD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAkBpH"}
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// One line for a person: green, amber or red, what it means, and the one action that fixes it. The tray icon (Windows) and
|
|
2
|
+
// the notifications (macOS, Linux) show this; nothing else decides colour.
|
|
3
|
+
export function healthOf(status, selfCheck) {
|
|
4
|
+
if (status.state === "not_governing") {
|
|
5
|
+
return { level: "red", headline: "Scopebond is not checking any coding agent on this computer", fix: { label: "Repair", route: "/repair" }, hint: null };
|
|
6
|
+
}
|
|
7
|
+
if (!status.delivery.connected) {
|
|
8
|
+
return { level: "red", headline: "Not connected to a workspace: records stay on this computer", fix: null, hint: "Connect it: npx @scopebond/hook login https://<your-workspace>" };
|
|
9
|
+
}
|
|
10
|
+
if (status.delivery.connection_refused_since !== null) {
|
|
11
|
+
return { level: "red", headline: "The workspace refuses this computer's connection", fix: null, hint: "Sign in again: npx @scopebond/hook login https://<your-workspace>" };
|
|
12
|
+
}
|
|
13
|
+
if (status.state === "recording_locally") {
|
|
14
|
+
const n = status.delivery.pending;
|
|
15
|
+
return { level: "amber", headline: `${n} record${n === 1 ? "" : "s"} waiting to be sent`, fix: { label: "Send now", route: "/flush" }, hint: null };
|
|
16
|
+
}
|
|
17
|
+
if (selfCheck && !selfCheck.ok) {
|
|
18
|
+
return { level: "amber", headline: `The daily self-check failed: ${selfCheck.failed.join(", ")}`, fix: { label: "Check again", route: "/maintain" }, hint: null };
|
|
19
|
+
}
|
|
20
|
+
return { level: "green", headline: "Checking your coding agents and delivering to your workspace", fix: null, hint: null };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"AAAA,2HAA2H;AAC3H,2EAA2E;AAc3E,MAAM,UAAU,QAAQ,CAAC,MAAkB,EAAE,SAA+D;IAC1G,IAAI,MAAM,CAAC,KAAK,KAAK,eAAe,EAAE,CAAC;QACrC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,6DAA6D,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC3J,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,6DAA6D,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,gEAAgE,EAAE,CAAC;IACtL,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,wBAAwB,KAAK,IAAI,EAAE,CAAC;QACtD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,kDAAkD,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,mEAAmE,EAAE,CAAC;IAC9K,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,KAAK,mBAAmB,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QAClC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,qBAAqB,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACtJ,CAAC;IACD,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,gCAAgC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACpK,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,8DAA8D,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC7H,CAAC","sourcesContent":["// One line for a person: green, amber or red, what it means, and the one action that fixes it. The tray icon (Windows) and\n// the notifications (macOS, Linux) show this; nothing else decides colour.\n\nimport type { StatusJson } from \"@scopebond/hook\";\n\nexport type HealthLevel = \"green\" | \"amber\" | \"red\";\nexport interface Health {\n level: HealthLevel;\n headline: string;\n /** The local-channel route that fixes it, with its button label, when one exists. */\n fix: { label: string; route: \"/repair\" | \"/flush\" | \"/maintain\" } | null;\n /** What a person types when no button can fix it. */\n hint: string | null;\n}\n\nexport function healthOf(status: StatusJson, selfCheck: { ok: boolean; failed: string[] } | null | undefined): Health {\n if (status.state === \"not_governing\") {\n return { level: \"red\", headline: \"Scopebond is not checking any coding agent on this computer\", fix: { label: \"Repair\", route: \"/repair\" }, hint: null };\n }\n if (!status.delivery.connected) {\n return { level: \"red\", headline: \"Not connected to a workspace: records stay on this computer\", fix: null, hint: \"Connect it: npx @scopebond/hook login https://<your-workspace>\" };\n }\n if (status.delivery.connection_refused_since !== null) {\n return { level: \"red\", headline: \"The workspace refuses this computer's connection\", fix: null, hint: \"Sign in again: npx @scopebond/hook login https://<your-workspace>\" };\n }\n if (status.state === \"recording_locally\") {\n const n = status.delivery.pending;\n return { level: \"amber\", headline: `${n} record${n === 1 ? \"\" : \"s\"} waiting to be sent`, fix: { label: \"Send now\", route: \"/flush\" }, hint: null };\n }\n if (selfCheck && !selfCheck.ok) {\n return { level: \"amber\", headline: `The daily self-check failed: ${selfCheck.failed.join(\", \")}`, fix: { label: \"Check again\", route: \"/maintain\" }, hint: null };\n }\n return { level: \"green\", headline: \"Checking your coding agents and delivering to your workspace\", fix: null, hint: null };\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,5 +8,8 @@ export { launcherPath, windowsLauncher, posixLauncher, windowsRunCommand, macLau
|
|
|
8
8
|
export { agentVersion, fetchClientVersion, compareVersions, commandHookVersion, maintainedHookCommand, maintainHookEntries, installAgent, } from "./update.js";
|
|
9
9
|
export type { ClientVersion } from "./update.js";
|
|
10
10
|
export { selfCheckProof, localChecks, runSelfCheck } from "./selfcheck.js";
|
|
11
|
+
export { parseQuestion, questionText, windowsScript, systemPrompter, serialized } from "./prompt.js";
|
|
12
|
+
export type { OverrideQuestion, OverrideAnswer, Prompter } from "./prompt.js";
|
|
13
|
+
export { queueReason, flushReasons, pendingReasons, REASONS_FILE } from "./override-reasons.js";
|
|
11
14
|
export type { SelfCheckItem } from "./selfcheck.js";
|
|
12
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC7F,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC/G,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EACL,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAC9F,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,KAAK,GAC1E,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,YAAY,GAChI,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3E,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC7F,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC/G,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EACL,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAC9F,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,KAAK,GAC1E,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,YAAY,GAChI,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrG,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChG,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,6 @@ export { startControl, callAgent, readEndpoint, AGENT_FILE, TOKEN_HEADER } from
|
|
|
4
4
|
export { launcherPath, windowsLauncher, posixLauncher, windowsRunCommand, macLaunchAgent, linuxUserUnit, enableAutostart, disableAutostart, autostartHealth, autostartPaths, LABEL, } from "./autostart.js";
|
|
5
5
|
export { agentVersion, fetchClientVersion, compareVersions, commandHookVersion, maintainedHookCommand, maintainHookEntries, installAgent, } from "./update.js";
|
|
6
6
|
export { selfCheckProof, localChecks, runSelfCheck } from "./selfcheck.js";
|
|
7
|
+
export { parseQuestion, questionText, windowsScript, systemPrompter, serialized } from "./prompt.js";
|
|
8
|
+
export { queueReason, flushReasons, pendingReasons, REASONS_FILE } from "./override-reasons.js";
|
|
7
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE7F,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE/G,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE3F,OAAO,EACL,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAC9F,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,KAAK,GAC1E,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,YAAY,GAChI,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["export { runCycle, computerStatus, expectedHarnesses, missingHookEntries } from \"./agent.js\";\nexport type { CycleOptions, CycleResult } from \"./agent.js\";\nexport { startService, repairHookEntries, spawnReplacement, AGENT_VERSION, AFTER_PID_ENV } from \"./service.js\";\nexport type { Service, ServiceOptions, MaintenanceResult } from \"./service.js\";\nexport { startControl, callAgent, readEndpoint, AGENT_FILE, TOKEN_HEADER } from \"./ipc.js\";\nexport type { AgentEndpoint } from \"./ipc.js\";\nexport {\n launcherPath, windowsLauncher, posixLauncher, windowsRunCommand, macLaunchAgent, linuxUserUnit,\n enableAutostart, disableAutostart, autostartHealth, autostartPaths, LABEL,\n} from \"./autostart.js\";\nexport {\n agentVersion, fetchClientVersion, compareVersions, commandHookVersion, maintainedHookCommand, maintainHookEntries, installAgent,\n} from \"./update.js\";\nexport type { ClientVersion } from \"./update.js\";\nexport { selfCheckProof, localChecks, runSelfCheck } from \"./selfcheck.js\";\nexport type { SelfCheckItem } from \"./selfcheck.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE7F,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE/G,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE3F,OAAO,EACL,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAC9F,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,KAAK,GAC1E,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,YAAY,GAChI,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["export { runCycle, computerStatus, expectedHarnesses, missingHookEntries } from \"./agent.js\";\nexport type { CycleOptions, CycleResult } from \"./agent.js\";\nexport { startService, repairHookEntries, spawnReplacement, AGENT_VERSION, AFTER_PID_ENV } from \"./service.js\";\nexport type { Service, ServiceOptions, MaintenanceResult } from \"./service.js\";\nexport { startControl, callAgent, readEndpoint, AGENT_FILE, TOKEN_HEADER } from \"./ipc.js\";\nexport type { AgentEndpoint } from \"./ipc.js\";\nexport {\n launcherPath, windowsLauncher, posixLauncher, windowsRunCommand, macLaunchAgent, linuxUserUnit,\n enableAutostart, disableAutostart, autostartHealth, autostartPaths, LABEL,\n} from \"./autostart.js\";\nexport {\n agentVersion, fetchClientVersion, compareVersions, commandHookVersion, maintainedHookCommand, maintainHookEntries, installAgent,\n} from \"./update.js\";\nexport type { ClientVersion } from \"./update.js\";\nexport { selfCheckProof, localChecks, runSelfCheck } from \"./selfcheck.js\";\nexport { parseQuestion, questionText, windowsScript, systemPrompter, serialized } from \"./prompt.js\";\nexport type { OverrideQuestion, OverrideAnswer, Prompter } from \"./prompt.js\";\nexport { queueReason, flushReasons, pendingReasons, REASONS_FILE } from \"./override-reasons.js\";\nexport type { SelfCheckItem } from \"./selfcheck.js\";\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HookConnection } from "@scopebond/hook";
|
|
2
|
+
export declare const REASONS_FILE = "override-reasons.json";
|
|
3
|
+
export declare function queueReason(dir: string, actionId: string, reason: string, now?: number): void;
|
|
4
|
+
export declare const pendingReasons: (dir: string) => number;
|
|
5
|
+
/** Send what is waiting. Returns how many the workspace took. A refusal for a malformed entry drops it; anything else waits. */
|
|
6
|
+
export declare function flushReasons(dir: string, connection: Pick<HookConnection, "url" | "credential">, fetchImpl?: typeof fetch): Promise<number>;
|
|
7
|
+
//# sourceMappingURL=override-reasons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"override-reasons.d.ts","sourceRoot":"","sources":["../src/override-reasons.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,eAAO,MAAM,YAAY,0BAA0B,CAAC;AAapD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,IAAI,CAKjG;AAED,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAA0B,CAAC;AAExE,gIAAgI;AAChI,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,YAAY,CAAC,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBxJ"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The reason a person gave in the Scopebond window goes to the workspace once (`POST /v1/overrides`); the receipt carries
|
|
2
|
+
// only its digest. Reasons wait in a small file in the Scopebond home until the workspace has them, so a reason given while
|
|
3
|
+
// offline still arrives.
|
|
4
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
export const REASONS_FILE = "override-reasons.json";
|
|
7
|
+
const read = (dir) => {
|
|
8
|
+
try {
|
|
9
|
+
const v = JSON.parse(readFileSync(join(dir, REASONS_FILE), "utf8"));
|
|
10
|
+
return Array.isArray(v) ? v : [];
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const write = (dir, list) => {
|
|
17
|
+
const file = join(dir, REASONS_FILE);
|
|
18
|
+
if (!list.length && !existsSync(file))
|
|
19
|
+
return;
|
|
20
|
+
writeFileSync(`${file}.tmp`, JSON.stringify(list), { mode: 0o600 });
|
|
21
|
+
renameSync(`${file}.tmp`, file);
|
|
22
|
+
};
|
|
23
|
+
export function queueReason(dir, actionId, reason, now = Date.now()) {
|
|
24
|
+
// A week is long enough for any computer to come back online; older reasons are dropped rather than kept forever.
|
|
25
|
+
const list = read(dir).filter((p) => now - p.at < 7 * 24 * 60 * 60 * 1000 && p.action_id !== actionId);
|
|
26
|
+
list.push({ action_id: actionId, reason, at: now });
|
|
27
|
+
write(dir, list.slice(-200));
|
|
28
|
+
}
|
|
29
|
+
export const pendingReasons = (dir) => read(dir).length;
|
|
30
|
+
/** Send what is waiting. Returns how many the workspace took. A refusal for a malformed entry drops it; anything else waits. */
|
|
31
|
+
export async function flushReasons(dir, connection, fetchImpl = fetch) {
|
|
32
|
+
const list = read(dir);
|
|
33
|
+
if (!list.length)
|
|
34
|
+
return 0;
|
|
35
|
+
const left = [];
|
|
36
|
+
let sent = 0;
|
|
37
|
+
for (const p of list) {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetchImpl(new URL("/v1/overrides", connection.url).toString(), {
|
|
40
|
+
method: "POST", redirect: "error", signal: AbortSignal.timeout(10_000),
|
|
41
|
+
headers: { authorization: `Bearer ${connection.credential}`, "content-type": "application/json" },
|
|
42
|
+
body: JSON.stringify({ action_id: p.action_id, reason: p.reason }),
|
|
43
|
+
});
|
|
44
|
+
if (res.ok)
|
|
45
|
+
sent += 1;
|
|
46
|
+
else if (res.status !== 400)
|
|
47
|
+
left.push(p);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
left.push(p);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
write(dir, left);
|
|
54
|
+
return sent;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=override-reasons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"override-reasons.js","sourceRoot":"","sources":["../src/override-reasons.ts"],"names":[],"mappings":"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","sourcesContent":["// The reason a person gave in the Scopebond window goes to the workspace once (`POST /v1/overrides`); the receipt carries\n// only its digest. Reasons wait in a small file in the Scopebond home until the workspace has them, so a reason given while\n// offline still arrives.\n\nimport { existsSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { HookConnection } from \"@scopebond/hook\";\n\nexport const REASONS_FILE = \"override-reasons.json\";\ninterface Pending { action_id: string; reason: string; at: number }\n\nconst read = (dir: string): Pending[] => {\n try { const v = JSON.parse(readFileSync(join(dir, REASONS_FILE), \"utf8\")) as Pending[]; return Array.isArray(v) ? v : []; } catch { return []; }\n};\nconst write = (dir: string, list: Pending[]) => {\n const file = join(dir, REASONS_FILE);\n if (!list.length && !existsSync(file)) return;\n writeFileSync(`${file}.tmp`, JSON.stringify(list), { mode: 0o600 });\n renameSync(`${file}.tmp`, file);\n};\n\nexport function queueReason(dir: string, actionId: string, reason: string, now = Date.now()): void {\n // A week is long enough for any computer to come back online; older reasons are dropped rather than kept forever.\n const list = read(dir).filter((p) => now - p.at < 7 * 24 * 60 * 60 * 1000 && p.action_id !== actionId);\n list.push({ action_id: actionId, reason, at: now });\n write(dir, list.slice(-200));\n}\n\nexport const pendingReasons = (dir: string): number => read(dir).length;\n\n/** Send what is waiting. Returns how many the workspace took. A refusal for a malformed entry drops it; anything else waits. */\nexport async function flushReasons(dir: string, connection: Pick<HookConnection, \"url\" | \"credential\">, fetchImpl: typeof fetch = fetch): Promise<number> {\n const list = read(dir);\n if (!list.length) return 0;\n const left: Pending[] = [];\n let sent = 0;\n for (const p of list) {\n try {\n const res = await fetchImpl(new URL(\"/v1/overrides\", connection.url).toString(), {\n method: \"POST\", redirect: \"error\", signal: AbortSignal.timeout(10_000),\n headers: { authorization: `Bearer ${connection.credential}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ action_id: p.action_id, reason: p.reason }),\n });\n if (res.ok) sent += 1;\n else if (res.status !== 400) left.push(p);\n } catch { left.push(p); }\n }\n write(dir, left);\n return sent;\n}\n"]}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface OverrideQuestion {
|
|
2
|
+
action_id: string;
|
|
3
|
+
rule: string;
|
|
4
|
+
title: string;
|
|
5
|
+
summary: string;
|
|
6
|
+
reason_min: number;
|
|
7
|
+
lasts: string;
|
|
8
|
+
timeout_ms: number;
|
|
9
|
+
}
|
|
10
|
+
export interface OverrideAnswer {
|
|
11
|
+
decision: "allow" | "deny" | "unavailable";
|
|
12
|
+
reason?: string;
|
|
13
|
+
os_user?: string;
|
|
14
|
+
}
|
|
15
|
+
export type Prompter = (question: OverrideQuestion) => Promise<OverrideAnswer>;
|
|
16
|
+
/** Check a request from the local channel. Anything malformed is refused before a window opens. */
|
|
17
|
+
export declare function parseQuestion(raw: unknown): OverrideQuestion | null;
|
|
18
|
+
export declare function questionText(q: OverrideQuestion): string;
|
|
19
|
+
/** Windows: a small topmost form from Windows' own signed PowerShell. Closes itself at the timeout. */
|
|
20
|
+
export declare function windowsScript(q: OverrideQuestion): string;
|
|
21
|
+
/** The window for this operating system. A reason shorter than the workspace accepts is refused here as well. */
|
|
22
|
+
export declare const systemPrompter: Prompter;
|
|
23
|
+
/** One window at a time: a second request waits for the first, and gives up as no answer if it would wait too long. */
|
|
24
|
+
export declare function serialized(prompter: Prompter): Prompter;
|
|
25
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AACD,MAAM,WAAW,cAAc;IAAG,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE;AACjH,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,gBAAgB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAK/E,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAWnE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAExD;AAiBD,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CA2CzD;AA0CD,iHAAiH;AACjH,eAAO,MAAM,cAAc,EAAE,QAQ5B,CAAC;AAEF,uHAAuH;AACvH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAQvD"}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// The Scopebond window: asks the person at the computer whether to allow one action a workspace rule blocks, with a reason.
|
|
2
|
+
// It is shown by this resident agent with the operating system's own tools (Windows PowerShell with Windows Forms, macOS
|
|
3
|
+
// osascript, Linux zenity), never by the coding agent, and the answer comes only from the window. Text reaches each tool as
|
|
4
|
+
// data (base64 for PowerShell, arguments for osascript and zenity), never spliced into code.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { userInfo } from "node:os";
|
|
7
|
+
const ID = /^[A-Za-z0-9._:-]{16,200}$/;
|
|
8
|
+
const clip = (v, max) => (typeof v === "string" ? v.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, max) : "");
|
|
9
|
+
/** Check a request from the local channel. Anything malformed is refused before a window opens. */
|
|
10
|
+
export function parseQuestion(raw) {
|
|
11
|
+
if (!raw || typeof raw !== "object")
|
|
12
|
+
return null;
|
|
13
|
+
const r = raw;
|
|
14
|
+
if (typeof r.action_id !== "string" || !ID.test(r.action_id) || typeof r.rule !== "string" || !/^[a-z0-9-]{1,64}$/.test(r.rule))
|
|
15
|
+
return null;
|
|
16
|
+
const reasonMin = Number(r.reason_min);
|
|
17
|
+
const timeout = Number(r.timeout_ms);
|
|
18
|
+
if (!Number.isInteger(reasonMin) || reasonMin < 10 || reasonMin > 200)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
action_id: r.action_id, rule: r.rule, title: clip(r.title, 120) || r.rule, summary: clip(r.summary, 200), reason_min: reasonMin,
|
|
22
|
+
lasts: clip(r.lasts, 80) || "this action only", timeout_ms: Number.isFinite(timeout) ? Math.min(Math.max(timeout, 5_000), 55_000) : 45_000,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function questionText(q) {
|
|
26
|
+
return `Your workspace's rule "${q.title}" blocked this action:\n\n${q.summary}\n\nYou may allow it (${q.lasts}). Say why, in at least ${q.reason_min} characters. Your name, the reason and the action are recorded in your workspace.`;
|
|
27
|
+
}
|
|
28
|
+
function run(command, args, timeoutMs) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
let stdout = "";
|
|
31
|
+
let child;
|
|
32
|
+
try {
|
|
33
|
+
child = spawn(command, args, { windowsHide: false, stdio: ["ignore", "pipe", "ignore"] });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
resolve({ code: null, stdout: "" });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const timer = setTimeout(() => { child.kill(); resolve({ code: null, stdout }); }, timeoutMs + 3_000);
|
|
40
|
+
child.stdout?.on("data", (d) => { stdout += d; });
|
|
41
|
+
child.on("error", () => { clearTimeout(timer); resolve({ code: null, stdout: "" }); });
|
|
42
|
+
child.on("close", (code) => { clearTimeout(timer); resolve({ code, stdout }); });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
46
|
+
/** Windows: a small topmost form from Windows' own signed PowerShell. Closes itself at the timeout. */
|
|
47
|
+
export function windowsScript(q) {
|
|
48
|
+
return `
|
|
49
|
+
$ErrorActionPreference = 'Stop'
|
|
50
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
51
|
+
Add-Type -AssemblyName System.Drawing
|
|
52
|
+
function T($s) { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)) }
|
|
53
|
+
$min = ${q.reason_min}
|
|
54
|
+
$f = New-Object Windows.Forms.Form
|
|
55
|
+
$f.Text = 'Scopebond'
|
|
56
|
+
$f.TopMost = $true
|
|
57
|
+
$f.StartPosition = 'CenterScreen'
|
|
58
|
+
$f.FormBorderStyle = 'FixedDialog'
|
|
59
|
+
$f.MaximizeBox = $false
|
|
60
|
+
$f.MinimizeBox = $false
|
|
61
|
+
$f.ClientSize = New-Object Drawing.Size(460, 250)
|
|
62
|
+
$l = New-Object Windows.Forms.Label
|
|
63
|
+
$l.Text = (T '${b64(questionText(q))}')
|
|
64
|
+
$l.SetBounds(12, 10, 436, 130)
|
|
65
|
+
$r = New-Object Windows.Forms.TextBox
|
|
66
|
+
$r.SetBounds(12, 145, 436, 24)
|
|
67
|
+
$a = New-Object Windows.Forms.Button
|
|
68
|
+
$a.Text = (T '${b64("Allow once")}')
|
|
69
|
+
$a.SetBounds(256, 200, 92, 30)
|
|
70
|
+
$a.Enabled = $false
|
|
71
|
+
$d = New-Object Windows.Forms.Button
|
|
72
|
+
$d.Text = (T '${b64("Don't allow")}')
|
|
73
|
+
$d.SetBounds(356, 200, 92, 30)
|
|
74
|
+
$r.Add_TextChanged({ $a.Enabled = ($r.Text.Trim().Length -ge $min) })
|
|
75
|
+
$script:answer = 'deny'
|
|
76
|
+
$a.Add_Click({ $script:answer = 'allow'; $f.Close() })
|
|
77
|
+
$d.Add_Click({ $f.Close() })
|
|
78
|
+
$f.AcceptButton = $d
|
|
79
|
+
$f.CancelButton = $d
|
|
80
|
+
$f.Controls.AddRange(@($l, $r, $a, $d))
|
|
81
|
+
$t = New-Object Windows.Forms.Timer
|
|
82
|
+
$t.Interval = ${q.timeout_ms}
|
|
83
|
+
$t.Add_Tick({ $t.Stop(); $f.Close() })
|
|
84
|
+
$t.Start()
|
|
85
|
+
$f.Add_Shown({ $f.Activate(); $r.Focus() })
|
|
86
|
+
[void]$f.ShowDialog()
|
|
87
|
+
$out = @{ decision = $script:answer; reason = $(if ($script:answer -eq 'allow') { $r.Text.Trim() } else { '' }) }
|
|
88
|
+
[Console]::Out.Write((ConvertTo-Json $out -Compress))
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
async function promptWindows(q) {
|
|
92
|
+
const encoded = Buffer.from(windowsScript(q), "utf16le").toString("base64");
|
|
93
|
+
const { code, stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-STA", "-WindowStyle", "Hidden", "-EncodedCommand", encoded], q.timeout_ms);
|
|
94
|
+
if (code !== 0)
|
|
95
|
+
return { decision: "unavailable" };
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(stdout.trim());
|
|
98
|
+
return parsed.decision === "allow" ? { decision: "allow", reason: String(parsed.reason ?? "") } : { decision: "deny" };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { decision: "unavailable" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const MAC_SCRIPT = [
|
|
105
|
+
"on run argv",
|
|
106
|
+
"set q to item 1 of argv",
|
|
107
|
+
"set secs to (item 2 of argv) as integer",
|
|
108
|
+
"set r to display dialog q default answer \"\" with title \"Scopebond\" buttons {\"Don't allow\", \"Allow once\"} default button \"Don't allow\" cancel button \"Don't allow\" giving up after secs",
|
|
109
|
+
"if gave up of r then return \"timeout\"",
|
|
110
|
+
"return \"allow\" & linefeed & (text returned of r)",
|
|
111
|
+
"end run",
|
|
112
|
+
];
|
|
113
|
+
async function promptMac(q) {
|
|
114
|
+
const args = MAC_SCRIPT.flatMap((line) => ["-e", line]);
|
|
115
|
+
const { code, stdout } = await run("osascript", [...args, questionText(q), String(Math.round(q.timeout_ms / 1000))], q.timeout_ms);
|
|
116
|
+
if (code === null)
|
|
117
|
+
return { decision: "unavailable" };
|
|
118
|
+
if (code !== 0)
|
|
119
|
+
return { decision: "deny" }; // "Don't allow" is the cancel button
|
|
120
|
+
const [first, ...rest] = stdout.replace(/\r?\n$/, "").split("\n");
|
|
121
|
+
return first === "allow" ? { decision: "allow", reason: rest.join("\n") } : { decision: "deny" };
|
|
122
|
+
}
|
|
123
|
+
async function promptLinux(q) {
|
|
124
|
+
const { code, stdout } = await run("zenity", [
|
|
125
|
+
"--entry", "--title=Scopebond", `--text=${questionText(q)}`, "--ok-label=Allow once", "--cancel-label=Don't allow",
|
|
126
|
+
`--timeout=${Math.round(q.timeout_ms / 1000)}`,
|
|
127
|
+
], q.timeout_ms);
|
|
128
|
+
if (code === null)
|
|
129
|
+
return { decision: "unavailable" };
|
|
130
|
+
if (code === 0)
|
|
131
|
+
return { decision: "allow", reason: stdout.replace(/\r?\n$/, "") };
|
|
132
|
+
// 1: Don't allow; 5: timed out; anything else (no display, not installed): no window.
|
|
133
|
+
return code === 1 || code === 5 ? { decision: "deny" } : { decision: "unavailable" };
|
|
134
|
+
}
|
|
135
|
+
/** The window for this operating system. A reason shorter than the workspace accepts is refused here as well. */
|
|
136
|
+
export const systemPrompter = async (q) => {
|
|
137
|
+
const answer = process.platform === "win32" ? await promptWindows(q) : process.platform === "darwin" ? await promptMac(q) : await promptLinux(q);
|
|
138
|
+
if (answer.decision !== "allow")
|
|
139
|
+
return answer;
|
|
140
|
+
const reason = (answer.reason ?? "").trim();
|
|
141
|
+
if (reason.length < q.reason_min)
|
|
142
|
+
return { decision: "deny" };
|
|
143
|
+
let os_user;
|
|
144
|
+
try {
|
|
145
|
+
os_user = userInfo().username;
|
|
146
|
+
}
|
|
147
|
+
catch { /* not known */ }
|
|
148
|
+
return { decision: "allow", reason: reason.slice(0, 500), ...(os_user ? { os_user } : {}) };
|
|
149
|
+
};
|
|
150
|
+
/** One window at a time: a second request waits for the first, and gives up as no answer if it would wait too long. */
|
|
151
|
+
export function serialized(prompter) {
|
|
152
|
+
let chain = Promise.resolve();
|
|
153
|
+
return (q) => {
|
|
154
|
+
const queuedAt = Date.now();
|
|
155
|
+
const next = chain.then(() => (Date.now() - queuedAt > q.timeout_ms ? { decision: "unavailable" } : prompter(q)));
|
|
156
|
+
chain = next.catch(() => undefined);
|
|
157
|
+
return next;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"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","sourcesContent":["// The Scopebond window: asks the person at the computer whether to allow one action a workspace rule blocks, with a reason.\n// It is shown by this resident agent with the operating system's own tools (Windows PowerShell with Windows Forms, macOS\n// osascript, Linux zenity), never by the coding agent, and the answer comes only from the window. Text reaches each tool as\n// data (base64 for PowerShell, arguments for osascript and zenity), never spliced into code.\n\nimport { spawn } from \"node:child_process\";\nimport { userInfo } from \"node:os\";\n\nexport interface OverrideQuestion {\n action_id: string;\n rule: string;\n title: string;\n summary: string;\n reason_min: number;\n lasts: string;\n timeout_ms: number;\n}\nexport interface OverrideAnswer { decision: \"allow\" | \"deny\" | \"unavailable\"; reason?: string; os_user?: string }\nexport type Prompter = (question: OverrideQuestion) => Promise<OverrideAnswer>;\n\nconst ID = /^[A-Za-z0-9._:-]{16,200}$/;\nconst clip = (v: unknown, max: number) => (typeof v === \"string\" ? v.replace(/[\\u0000-\\u001f\\u007f]/g, \" \").slice(0, max) : \"\");\n\n/** Check a request from the local channel. Anything malformed is refused before a window opens. */\nexport function parseQuestion(raw: unknown): OverrideQuestion | null {\n if (!raw || typeof raw !== \"object\") return null;\n const r = raw as Record<string, unknown>;\n if (typeof r.action_id !== \"string\" || !ID.test(r.action_id) || typeof r.rule !== \"string\" || !/^[a-z0-9-]{1,64}$/.test(r.rule)) return null;\n const reasonMin = Number(r.reason_min);\n const timeout = Number(r.timeout_ms);\n if (!Number.isInteger(reasonMin) || reasonMin < 10 || reasonMin > 200) return null;\n return {\n action_id: r.action_id, rule: r.rule, title: clip(r.title, 120) || r.rule, summary: clip(r.summary, 200), reason_min: reasonMin,\n lasts: clip(r.lasts, 80) || \"this action only\", timeout_ms: Number.isFinite(timeout) ? Math.min(Math.max(timeout, 5_000), 55_000) : 45_000,\n };\n}\n\nexport function questionText(q: OverrideQuestion): string {\n return `Your workspace's rule \"${q.title}\" blocked this action:\\n\\n${q.summary}\\n\\nYou may allow it (${q.lasts}). Say why, in at least ${q.reason_min} characters. Your name, the reason and the action are recorded in your workspace.`;\n}\n\nfunction run(command: string, args: string[], timeoutMs: number): Promise<{ code: number | null; stdout: string }> {\n return new Promise((resolve) => {\n let stdout = \"\";\n let child;\n try { child = spawn(command, args, { windowsHide: false, stdio: [\"ignore\", \"pipe\", \"ignore\"] }); }\n catch { resolve({ code: null, stdout: \"\" }); return; }\n const timer = setTimeout(() => { child.kill(); resolve({ code: null, stdout }); }, timeoutMs + 3_000);\n child.stdout?.on(\"data\", (d) => { stdout += d; });\n child.on(\"error\", () => { clearTimeout(timer); resolve({ code: null, stdout: \"\" }); });\n child.on(\"close\", (code) => { clearTimeout(timer); resolve({ code, stdout }); });\n });\n}\n\nconst b64 = (s: string) => Buffer.from(s, \"utf8\").toString(\"base64\");\n\n/** Windows: a small topmost form from Windows' own signed PowerShell. Closes itself at the timeout. */\nexport function windowsScript(q: OverrideQuestion): string {\n return `\n$ErrorActionPreference = 'Stop'\nAdd-Type -AssemblyName System.Windows.Forms\nAdd-Type -AssemblyName System.Drawing\nfunction T($s) { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)) }\n$min = ${q.reason_min}\n$f = New-Object Windows.Forms.Form\n$f.Text = 'Scopebond'\n$f.TopMost = $true\n$f.StartPosition = 'CenterScreen'\n$f.FormBorderStyle = 'FixedDialog'\n$f.MaximizeBox = $false\n$f.MinimizeBox = $false\n$f.ClientSize = New-Object Drawing.Size(460, 250)\n$l = New-Object Windows.Forms.Label\n$l.Text = (T '${b64(questionText(q))}')\n$l.SetBounds(12, 10, 436, 130)\n$r = New-Object Windows.Forms.TextBox\n$r.SetBounds(12, 145, 436, 24)\n$a = New-Object Windows.Forms.Button\n$a.Text = (T '${b64(\"Allow once\")}')\n$a.SetBounds(256, 200, 92, 30)\n$a.Enabled = $false\n$d = New-Object Windows.Forms.Button\n$d.Text = (T '${b64(\"Don't allow\")}')\n$d.SetBounds(356, 200, 92, 30)\n$r.Add_TextChanged({ $a.Enabled = ($r.Text.Trim().Length -ge $min) })\n$script:answer = 'deny'\n$a.Add_Click({ $script:answer = 'allow'; $f.Close() })\n$d.Add_Click({ $f.Close() })\n$f.AcceptButton = $d\n$f.CancelButton = $d\n$f.Controls.AddRange(@($l, $r, $a, $d))\n$t = New-Object Windows.Forms.Timer\n$t.Interval = ${q.timeout_ms}\n$t.Add_Tick({ $t.Stop(); $f.Close() })\n$t.Start()\n$f.Add_Shown({ $f.Activate(); $r.Focus() })\n[void]$f.ShowDialog()\n$out = @{ decision = $script:answer; reason = $(if ($script:answer -eq 'allow') { $r.Text.Trim() } else { '' }) }\n[Console]::Out.Write((ConvertTo-Json $out -Compress))\n`;\n}\n\nasync function promptWindows(q: OverrideQuestion): Promise<OverrideAnswer> {\n const encoded = Buffer.from(windowsScript(q), \"utf16le\").toString(\"base64\");\n const { code, stdout } = await run(\"powershell.exe\", [\"-NoProfile\", \"-NonInteractive\", \"-STA\", \"-WindowStyle\", \"Hidden\", \"-EncodedCommand\", encoded], q.timeout_ms);\n if (code !== 0) return { decision: \"unavailable\" };\n try {\n const parsed = JSON.parse(stdout.trim()) as { decision?: string; reason?: string };\n return parsed.decision === \"allow\" ? { decision: \"allow\", reason: String(parsed.reason ?? \"\") } : { decision: \"deny\" };\n } catch { return { decision: \"unavailable\" }; }\n}\n\nconst MAC_SCRIPT = [\n \"on run argv\",\n \"set q to item 1 of argv\",\n \"set secs to (item 2 of argv) as integer\",\n \"set r to display dialog q default answer \\\"\\\" with title \\\"Scopebond\\\" buttons {\\\"Don't allow\\\", \\\"Allow once\\\"} default button \\\"Don't allow\\\" cancel button \\\"Don't allow\\\" giving up after secs\",\n \"if gave up of r then return \\\"timeout\\\"\",\n \"return \\\"allow\\\" & linefeed & (text returned of r)\",\n \"end run\",\n];\n\nasync function promptMac(q: OverrideQuestion): Promise<OverrideAnswer> {\n const args = MAC_SCRIPT.flatMap((line) => [\"-e\", line]);\n const { code, stdout } = await run(\"osascript\", [...args, questionText(q), String(Math.round(q.timeout_ms / 1000))], q.timeout_ms);\n if (code === null) return { decision: \"unavailable\" };\n if (code !== 0) return { decision: \"deny\" }; // \"Don't allow\" is the cancel button\n const [first, ...rest] = stdout.replace(/\\r?\\n$/, \"\").split(\"\\n\");\n return first === \"allow\" ? { decision: \"allow\", reason: rest.join(\"\\n\") } : { decision: \"deny\" };\n}\n\nasync function promptLinux(q: OverrideQuestion): Promise<OverrideAnswer> {\n const { code, stdout } = await run(\"zenity\", [\n \"--entry\", \"--title=Scopebond\", `--text=${questionText(q)}`, \"--ok-label=Allow once\", \"--cancel-label=Don't allow\",\n `--timeout=${Math.round(q.timeout_ms / 1000)}`,\n ], q.timeout_ms);\n if (code === null) return { decision: \"unavailable\" };\n if (code === 0) return { decision: \"allow\", reason: stdout.replace(/\\r?\\n$/, \"\") };\n // 1: Don't allow; 5: timed out; anything else (no display, not installed): no window.\n return code === 1 || code === 5 ? { decision: \"deny\" } : { decision: \"unavailable\" };\n}\n\n/** The window for this operating system. A reason shorter than the workspace accepts is refused here as well. */\nexport const systemPrompter: Prompter = async (q) => {\n const answer = process.platform === \"win32\" ? await promptWindows(q) : process.platform === \"darwin\" ? await promptMac(q) : await promptLinux(q);\n if (answer.decision !== \"allow\") return answer;\n const reason = (answer.reason ?? \"\").trim();\n if (reason.length < q.reason_min) return { decision: \"deny\" };\n let os_user: string | undefined;\n try { os_user = userInfo().username; } catch { /* not known */ }\n return { decision: \"allow\", reason: reason.slice(0, 500), ...(os_user ? { os_user } : {}) };\n};\n\n/** One window at a time: a second request waits for the first, and gives up as no answer if it would wait too long. */\nexport function serialized(prompter: Prompter): Prompter {\n let chain: Promise<unknown> = Promise.resolve();\n return (q) => {\n const queuedAt = Date.now();\n const next = chain.then(() => (Date.now() - queuedAt > q.timeout_ms ? { decision: \"unavailable\" as const } : prompter(q)));\n chain = next.catch(() => undefined);\n return next;\n };\n}\n"]}
|
package/dist/selfcheck.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selfcheck.d.ts","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,OAAO,EAAE,KAAK,cAAc,EAClC,MAAM,iBAAiB,CAAC;AAGzB,MAAM,WAAW,aAAa;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AAE3E,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,GAAG,SAAa,GAAG,aAAa,EAAE,CAgB5H;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EACnF,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3E,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"selfcheck.d.ts","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,OAAO,EAAE,KAAK,cAAc,EAClC,MAAM,iBAAiB,CAAC;AAGzB,MAAM,WAAW,aAAa;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AAE3E,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,GAAG,SAAa,GAAG,aAAa,EAAE,CAgB5H;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EACnF,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3E,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC,CAuBzG"}
|
package/dist/selfcheck.js
CHANGED
|
@@ -43,6 +43,12 @@ export async function runSelfCheck(dir, connection, harnesses, agentVersion, opt
|
|
|
43
43
|
headers: { authorization: `Bearer ${connection.credential}`, "content-type": "application/json" },
|
|
44
44
|
body: JSON.stringify({ signature, day, checks, hook_version: hookVersion(), agent_version: agentVersion }),
|
|
45
45
|
});
|
|
46
|
+
// A workspace that does not offer the self-check yet answers 404: the computer's own checks still count; the signed
|
|
47
|
+
// workspace part is simply not available there.
|
|
48
|
+
if (res.status === 404) {
|
|
49
|
+
const failed = checks.filter((c) => !c.ok).map((c) => c.id);
|
|
50
|
+
return { ok: failed.length === 0, signature_verified: false, failed, checks };
|
|
51
|
+
}
|
|
46
52
|
if (!res.ok)
|
|
47
53
|
return { ok: false, signature_verified: false, failed: [`workspace_http_${res.status}`], checks };
|
|
48
54
|
const body = await res.json();
|
package/dist/selfcheck.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selfcheck.js","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"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","sourcesContent":["// The daily end-to-end self-check (SB347). The agent checks this computer's side (the hook entry\n// is present and starts, autostart works, nothing is stuck, the connection is not about to lapse),\n// then signs the day with the key it enrolled with and sends everything to the workspace, which\n// verifies the signature. A pass means the whole path works, end to end; a failure names what broke.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { canonical } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport {\n configuredHookCommands, hookCommandResolves, hookVersion, isScopebondHookCommand, queueStatus, readDeliveryState, userHarnessFile,\n type Harness, type HookConnection,\n} from \"@scopebond/hook\";\nimport { autostartHealth } from \"./autostart.js\";\n\nexport interface SelfCheckItem { id: string; ok: boolean; detail?: string }\n\nexport function selfCheckProof(credentialId: string, gatewayId: string, day: string): string {\n return canonical({ type: \"scopebond:self-check\", version: 1, credential_id: credentialId, gateway_id: gatewayId, day });\n}\n\nexport function localChecks(dir: string, connection: HookConnection, harnesses: Harness[], now = Date.now()): SelfCheckItem[] {\n const checks: SelfCheckItem[] = [];\n const entries = harnesses.flatMap((h) => configuredHookCommands(userHarnessFile(h)).filter((c) => isScopebondHookCommand(c)));\n checks.push({ id: \"hook_entry\", ok: entries.length > 0, ...(entries.length ? {} : { detail: \"no agent setting holds the Scopebond hook\" }) });\n const broken = entries.filter((c) => !hookCommandResolves(c));\n checks.push({ id: \"hook_starts\", ok: entries.length > 0 && broken.length === 0, ...(broken.length ? { detail: `${broken.length} hook command(s) cannot start` } : {}) });\n const autostart = autostartHealth(dir);\n checks.push({ id: \"autostart\", ok: autostart.ok, ...(autostart.ok ? {} : { detail: autostart.detail }) });\n const { pending, oldest } = queueStatus(dir);\n const state = readDeliveryState(dir);\n const stuck = pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000;\n checks.push({ id: \"queue\", ok: !stuck, ...(stuck ? { detail: `${pending} record(s) waiting since ${new Date(oldest!).toISOString()}${state.last_error ? ` (${state.last_error.slice(0, 120)})` : \"\"}` } : {}) });\n const expires = Date.parse(connection.expires_at ?? \"\");\n const lapsing = Number.isFinite(expires) && expires - now < 7 * 24 * 60 * 60 * 1000;\n checks.push({ id: \"credential\", ok: !lapsing, ...(lapsing ? { detail: `the connection expires ${new Date(expires).toISOString()} and was not renewed` } : {}) });\n return checks;\n}\n\nexport async function runSelfCheck(\n dir: string, connection: HookConnection, harnesses: Harness[], agentVersion: string,\n options: { fetchImpl?: typeof fetch; now?: number; timeoutMs?: number } = {},\n): Promise<{ ok: boolean; signature_verified: boolean; failed: string[]; checks: SelfCheckItem[] } | null> {\n const now = options.now ?? Date.now();\n const checks = localChecks(dir, connection, harnesses, now);\n const keyFile = join(dir, \"attester.key\");\n if (!existsSync(keyFile)) return { ok: false, signature_verified: false, failed: [\"signing_key\"], checks };\n const day = new Date(now).toISOString().slice(0, 10);\n const { attester } = loadOrCreateAttester({ file: keyFile });\n const signature = await attester.sign(selfCheckProof(connection.credential_id, connection.gateway_id, day));\n try {\n const res = await (options.fetchImpl ?? fetch)(new URL(\"/v1/self-check\", connection.url).toString(), {\n method: \"POST\", redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n headers: { authorization: `Bearer ${connection.credential}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ signature, day, checks, hook_version: hookVersion(), agent_version: agentVersion }),\n });\n if (!res.ok) return { ok: false, signature_verified: false, failed: [`workspace_http_${res.status}`], checks };\n const body = await res.json() as { ok?: boolean; signature_verified?: boolean; failed?: string[] };\n return { ok: !!body.ok, signature_verified: !!body.signature_verified, failed: Array.isArray(body.failed) ? body.failed : [], checks };\n } catch (error) {\n return { ok: false, signature_verified: false, failed: [`workspace_unreachable: ${(error as Error).message.slice(0, 80)}`], checks };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"selfcheck.js","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"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","sourcesContent":["// The daily end-to-end self-check (SB347). The agent checks this computer's side (the hook entry\n// is present and starts, autostart works, nothing is stuck, the connection is not about to lapse),\n// then signs the day with the key it enrolled with and sends everything to the workspace, which\n// verifies the signature. A pass means the whole path works, end to end; a failure names what broke.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { canonical } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport {\n configuredHookCommands, hookCommandResolves, hookVersion, isScopebondHookCommand, queueStatus, readDeliveryState, userHarnessFile,\n type Harness, type HookConnection,\n} from \"@scopebond/hook\";\nimport { autostartHealth } from \"./autostart.js\";\n\nexport interface SelfCheckItem { id: string; ok: boolean; detail?: string }\n\nexport function selfCheckProof(credentialId: string, gatewayId: string, day: string): string {\n return canonical({ type: \"scopebond:self-check\", version: 1, credential_id: credentialId, gateway_id: gatewayId, day });\n}\n\nexport function localChecks(dir: string, connection: HookConnection, harnesses: Harness[], now = Date.now()): SelfCheckItem[] {\n const checks: SelfCheckItem[] = [];\n const entries = harnesses.flatMap((h) => configuredHookCommands(userHarnessFile(h)).filter((c) => isScopebondHookCommand(c)));\n checks.push({ id: \"hook_entry\", ok: entries.length > 0, ...(entries.length ? {} : { detail: \"no agent setting holds the Scopebond hook\" }) });\n const broken = entries.filter((c) => !hookCommandResolves(c));\n checks.push({ id: \"hook_starts\", ok: entries.length > 0 && broken.length === 0, ...(broken.length ? { detail: `${broken.length} hook command(s) cannot start` } : {}) });\n const autostart = autostartHealth(dir);\n checks.push({ id: \"autostart\", ok: autostart.ok, ...(autostart.ok ? {} : { detail: autostart.detail }) });\n const { pending, oldest } = queueStatus(dir);\n const state = readDeliveryState(dir);\n const stuck = pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000;\n checks.push({ id: \"queue\", ok: !stuck, ...(stuck ? { detail: `${pending} record(s) waiting since ${new Date(oldest!).toISOString()}${state.last_error ? ` (${state.last_error.slice(0, 120)})` : \"\"}` } : {}) });\n const expires = Date.parse(connection.expires_at ?? \"\");\n const lapsing = Number.isFinite(expires) && expires - now < 7 * 24 * 60 * 60 * 1000;\n checks.push({ id: \"credential\", ok: !lapsing, ...(lapsing ? { detail: `the connection expires ${new Date(expires).toISOString()} and was not renewed` } : {}) });\n return checks;\n}\n\nexport async function runSelfCheck(\n dir: string, connection: HookConnection, harnesses: Harness[], agentVersion: string,\n options: { fetchImpl?: typeof fetch; now?: number; timeoutMs?: number } = {},\n): Promise<{ ok: boolean; signature_verified: boolean; failed: string[]; checks: SelfCheckItem[] } | null> {\n const now = options.now ?? Date.now();\n const checks = localChecks(dir, connection, harnesses, now);\n const keyFile = join(dir, \"attester.key\");\n if (!existsSync(keyFile)) return { ok: false, signature_verified: false, failed: [\"signing_key\"], checks };\n const day = new Date(now).toISOString().slice(0, 10);\n const { attester } = loadOrCreateAttester({ file: keyFile });\n const signature = await attester.sign(selfCheckProof(connection.credential_id, connection.gateway_id, day));\n try {\n const res = await (options.fetchImpl ?? fetch)(new URL(\"/v1/self-check\", connection.url).toString(), {\n method: \"POST\", redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n headers: { authorization: `Bearer ${connection.credential}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ signature, day, checks, hook_version: hookVersion(), agent_version: agentVersion }),\n });\n // A workspace that does not offer the self-check yet answers 404: the computer's own checks still count; the signed\n // workspace part is simply not available there.\n if (res.status === 404) { const failed = checks.filter((c) => !c.ok).map((c) => c.id); return { ok: failed.length === 0, signature_verified: false, failed, checks }; }\n if (!res.ok) return { ok: false, signature_verified: false, failed: [`workspace_http_${res.status}`], checks };\n const body = await res.json() as { ok?: boolean; signature_verified?: boolean; failed?: string[] };\n return { ok: !!body.ok, signature_verified: !!body.signature_verified, failed: Array.isArray(body.failed) ? body.failed : [], checks };\n } catch (error) {\n return { ok: false, signature_verified: false, failed: [`workspace_unreachable: ${(error as Error).message.slice(0, 80)}`], checks };\n }\n}\n"]}
|
package/dist/service.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Harness } from "@scopebond/hook";
|
|
2
2
|
import { type CycleResult } from "./agent.js";
|
|
3
3
|
import { runSelfCheck } from "./selfcheck.js";
|
|
4
|
+
import { type Prompter } from "./prompt.js";
|
|
4
5
|
export declare const AGENT_VERSION: string;
|
|
5
6
|
export declare const AFTER_PID_ENV = "SCOPEBOND_AGENT_AFTER_PID";
|
|
6
7
|
export interface ServiceOptions {
|
|
@@ -12,6 +13,10 @@ export interface ServiceOptions {
|
|
|
12
13
|
maintenance?: boolean;
|
|
13
14
|
/** Called after a successful update instead of restarting (tests). */
|
|
14
15
|
onUpdated?: (version: string) => void;
|
|
16
|
+
/** Shows the Scopebond window for an override (tests replace it). */
|
|
17
|
+
prompter?: Prompter;
|
|
18
|
+
/** The Windows tray icon (default on Windows; SCOPEBOND_AGENT_TRAY=off turns it off). */
|
|
19
|
+
tray?: boolean;
|
|
15
20
|
}
|
|
16
21
|
export interface Service {
|
|
17
22
|
stop(): Promise<void>;
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiF,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC9H,OAAO,EAAmE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAG/G,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiF,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC9H,OAAO,EAAmE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAG/G,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAA6C,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKvF,eAAO,MAAM,aAAa,QAA4B,CAAC;AAKvD,eAAO,MAAM,aAAa,8BAA8B,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzF,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,yFAAyF;IACzF,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACjC,WAAW,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;IACpD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;sFACsF;AACtF,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,OAAO,EAAwB,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAOvH;AAED;uFACuF;AACvF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAsH5E"}
|
package/dist/service.js
CHANGED
|
@@ -10,6 +10,10 @@ import { computerStatus, expectedHarnesses, missingHookEntries, runCycle } from
|
|
|
10
10
|
import { launcherPath } from "./autostart.js";
|
|
11
11
|
import { callAgent, startControl } from "./ipc.js";
|
|
12
12
|
import { runSelfCheck } from "./selfcheck.js";
|
|
13
|
+
import { flushReasons, queueReason } from "./override-reasons.js";
|
|
14
|
+
import { parseQuestion, serialized, systemPrompter } from "./prompt.js";
|
|
15
|
+
import { healthOf } from "./health.js";
|
|
16
|
+
import { notifyChange, startTray } from "./tray.js";
|
|
13
17
|
import { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries } from "./update.js";
|
|
14
18
|
export const AGENT_VERSION = `agent/${agentVersion()}`;
|
|
15
19
|
const INTERVAL_MS = 60_000;
|
|
@@ -52,10 +56,30 @@ export async function startService(options) {
|
|
|
52
56
|
let running = null;
|
|
53
57
|
let stopped = false;
|
|
54
58
|
let lastSelfCheckAt = 0;
|
|
59
|
+
const prompt = serialized(options.prompter ?? systemPrompter);
|
|
60
|
+
let shownLevel = null;
|
|
61
|
+
const health = () => healthOf(computerStatus(options.dir), lastMaintenance?.selfCheck ?? null);
|
|
62
|
+
const noticeHealth = () => { const h = health(); notifyChange(shownLevel, h); shownLevel = h.level; };
|
|
63
|
+
const sendReasons = async () => {
|
|
64
|
+
const connection = loadConnection(options.dir);
|
|
65
|
+
if (!connection)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
const sent = await flushReasons(options.dir, connection, options.fetchImpl);
|
|
69
|
+
if (sent)
|
|
70
|
+
log(`sent ${sent} override reason(s) to the workspace`);
|
|
71
|
+
}
|
|
72
|
+
catch { /* they wait for the next cycle */ }
|
|
73
|
+
};
|
|
55
74
|
const cycle = async () => {
|
|
56
75
|
if (running)
|
|
57
76
|
return running;
|
|
58
|
-
running = runCycle({ dir: options.dir, fetchImpl: options.fetchImpl }).then((result) => {
|
|
77
|
+
running = runCycle({ dir: options.dir, fetchImpl: options.fetchImpl }).then(async (result) => {
|
|
78
|
+
await sendReasons();
|
|
79
|
+
try {
|
|
80
|
+
noticeHealth();
|
|
81
|
+
}
|
|
82
|
+
catch { /* status is best effort */ }
|
|
59
83
|
last = result;
|
|
60
84
|
failures = result.deliveryError ? failures + 1 : 0;
|
|
61
85
|
if (result.delivered)
|
|
@@ -101,7 +125,7 @@ export async function startService(options) {
|
|
|
101
125
|
if (connection && (forceSelfCheck || Date.now() - lastSelfCheckAt >= SELF_CHECK_EVERY_MS)) {
|
|
102
126
|
result.selfCheck = await runSelfCheck(options.dir, connection, harnesses, current, { fetchImpl: options.fetchImpl });
|
|
103
127
|
lastSelfCheckAt = Date.now();
|
|
104
|
-
log(`self-check ${result.selfCheck?.ok ? "passed" : `failed: ${result.selfCheck?.failed.join(", ")}`}`);
|
|
128
|
+
log(`self-check ${result.selfCheck?.ok ? "passed" : `failed: ${result.selfCheck?.failed.join(", ") ?? "no result"}`}`);
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
catch (error) {
|
|
@@ -117,7 +141,7 @@ export async function startService(options) {
|
|
|
117
141
|
timer = setTimeout(() => { void cycle().finally(schedule); }, delay);
|
|
118
142
|
};
|
|
119
143
|
const control = await startControl(options.dir, `${AGENT_VERSION} hook/${hookVersion()}`, {
|
|
120
|
-
"GET /status": () => ({ ...computerStatus(options.dir), agent: { pid: process.pid, version: AGENT_VERSION, last_cycle: last, last_maintenance: lastMaintenance } }),
|
|
144
|
+
"GET /status": () => ({ ...computerStatus(options.dir), health: health(), agent: { pid: process.pid, version: AGENT_VERSION, last_cycle: last, last_maintenance: lastMaintenance } }),
|
|
121
145
|
"POST /flush": async () => ({ cycle: await cycle() }),
|
|
122
146
|
"POST /repair": () => {
|
|
123
147
|
const repaired = repairHookEntries();
|
|
@@ -126,9 +150,28 @@ export async function startService(options) {
|
|
|
126
150
|
return { repaired };
|
|
127
151
|
},
|
|
128
152
|
"POST /maintain": async () => ({ maintenance: await maintain(true) }),
|
|
153
|
+
// Warn mode: the hook asks; only the window answers. The caller learns the answer, never decides it.
|
|
154
|
+
"POST /override": async (body) => {
|
|
155
|
+
const question = parseQuestion(body);
|
|
156
|
+
if (!question)
|
|
157
|
+
return { decision: "unavailable" };
|
|
158
|
+
log(`asking whether to allow "${question.title}"`);
|
|
159
|
+
const answer = await prompt(question);
|
|
160
|
+
log(`override ${answer.decision === "allow" ? "given" : answer.decision === "deny" ? "not given" : "not available"} for "${question.title}"`);
|
|
161
|
+
if (answer.decision === "allow" && answer.reason) {
|
|
162
|
+
queueReason(options.dir, question.action_id, answer.reason);
|
|
163
|
+
void sendReasons();
|
|
164
|
+
}
|
|
165
|
+
return answer;
|
|
166
|
+
},
|
|
129
167
|
});
|
|
168
|
+
const tray = (options.tray ?? true) && process.env.SCOPEBOND_AGENT_TRAY !== "off" ? startTray(options.dir) : null;
|
|
130
169
|
const stop = async () => {
|
|
131
170
|
stopped = true;
|
|
171
|
+
try {
|
|
172
|
+
tray?.kill();
|
|
173
|
+
}
|
|
174
|
+
catch { /* already gone */ }
|
|
132
175
|
if (timer)
|
|
133
176
|
clearTimeout(timer);
|
|
134
177
|
if (maintenanceTimer)
|
package/dist/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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","sourcesContent":["// The resident loop: one cycle now, then one every interval, sooner after a change is asked for,\n// backing off while the workspace is unreachable. Exactly one agent serves a Scopebond home: a\n// second one finds the first answering and exits. Every six hours it asks the workspace which\n// versions to run, and once a day it runs the end-to-end self-check.\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { hookCommand, hookVersion, loadConnection, userHarnessFile, writeHarnessConfig, type Harness } from \"@scopebond/hook\";\nimport { computerStatus, expectedHarnesses, missingHookEntries, runCycle, type CycleResult } from \"./agent.js\";\nimport { launcherPath } from \"./autostart.js\";\nimport { callAgent, startControl } from \"./ipc.js\";\nimport { runSelfCheck } from \"./selfcheck.js\";\nimport { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries } from \"./update.js\";\n\nexport const AGENT_VERSION = `agent/${agentVersion()}`;\nconst INTERVAL_MS = 60_000;\nconst MAX_BACKOFF_MS = 15 * 60_000;\nconst UPDATE_EVERY_MS = 6 * 60 * 60 * 1000;\nconst SELF_CHECK_EVERY_MS = 24 * 60 * 60 * 1000;\nexport const AFTER_PID_ENV = \"SCOPEBOND_AGENT_AFTER_PID\";\n\nexport interface ServiceOptions {\n dir: string; intervalMs?: number; fetchImpl?: typeof fetch; log?: (line: string) => void;\n /** Off in tests: the update check and self-check run only when asked. */\n maintenance?: boolean;\n /** Called after a successful update instead of restarting (tests). */\n onUpdated?: (version: string) => void;\n}\n\nexport interface Service {\n stop(): Promise<void>;\n cycleNow(): Promise<CycleResult>;\n maintainNow(): Promise<MaintenanceResult>;\n port: number;\n}\n\nexport interface MaintenanceResult {\n at: number;\n policy: \"recommended\" | \"hold\" | \"unknown\";\n updatedTo: string | null;\n hookEntries: Array<{ harness: Harness; file: string; reason: string }>;\n selfCheck: Awaited<ReturnType<typeof runSelfCheck>>;\n error: string | null;\n}\n\n/** Put the Scopebond hook back into agent settings that lost it. A visible change: each one is\n * logged and returned, and only settings of agents this computer has are touched. */\nexport function repairHookEntries(harnesses: Harness[] = expectedHarnesses()): Array<{ harness: Harness; file: string }> {\n const repaired: Array<{ harness: Harness; file: string }> = [];\n for (const harness of missingHookEntries(harnesses)) {\n const file = writeHarnessConfig(userHarnessFile(harness), harness, hookCommand(harness));\n repaired.push({ harness, file });\n }\n return repaired;\n}\n\n/** Start a fresh agent (the updated one) that waits for this process to exit, through the\n * autostart launcher when there is one so it finds Node the same way sign-in does. */\nexport function spawnReplacement(dir: string): void {\n const launcher = launcherPath(dir);\n const cli = realpathSync(fileURLToPath(new URL(\"./cli.js\", import.meta.url)));\n const env = { ...process.env, [AFTER_PID_ENV]: String(process.pid) };\n const [command, args] = existsSync(launcher)\n ? process.platform === \"win32\" ? [\"cmd.exe\", [\"/d\", \"/c\", launcher]] as const : [\"/bin/sh\", [launcher]] as const\n : [process.execPath, [cli, \"run\"]] as const;\n const child = spawn(command, [...args], { env, detached: true, stdio: \"ignore\", windowsHide: true });\n child.unref();\n}\n\nexport async function startService(options: ServiceOptions): Promise<Service> {\n const log = options.log ?? ((line: string) => console.log(`${new Date().toISOString()} ${line}`));\n if (await callAgent(options.dir, \"GET\", \"/status\", undefined, 2_000)) throw new Error(\"a Scopebond Agent is already running for this computer\");\n const interval = options.intervalMs ?? INTERVAL_MS;\n let last: CycleResult | null = null;\n let lastMaintenance: MaintenanceResult | null = null;\n let failures = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let maintenanceTimer: ReturnType<typeof setInterval> | null = null;\n let running: Promise<CycleResult> | null = null;\n let stopped = false;\n let lastSelfCheckAt = 0;\n\n const cycle = async (): Promise<CycleResult> => {\n if (running) return running;\n running = runCycle({ dir: options.dir, fetchImpl: options.fetchImpl }).then((result) => {\n last = result;\n failures = result.deliveryError ? failures + 1 : 0;\n if (result.delivered) log(`delivered ${result.delivered} record(s); ${result.pending} waiting`);\n if (result.deliveryError) log(`delivery problem: ${result.deliveryError}`);\n return result;\n }).finally(() => { running = null; });\n return running;\n };\n\n const maintain = async (forceSelfCheck = false): Promise<MaintenanceResult> => {\n const result: MaintenanceResult = { at: Date.now(), policy: \"unknown\", updatedTo: null, hookEntries: [], selfCheck: null, error: null };\n try {\n const connection = loadConnection(options.dir);\n const harnesses = expectedHarnesses();\n const target = connection ? await fetchClientVersion(connection, options.fetchImpl) : null;\n if (target) result.policy = target.policy;\n const current = agentVersion();\n if (target?.policy === \"recommended\" && target.agent && compareVersions(target.agent, current) > 0) {\n log(`updating the Scopebond Agent ${current} -> ${target.agent}`);\n const installed = await installAgent(target.agent);\n if (installed.ok) {\n result.updatedTo = target.agent;\n lastMaintenance = result;\n if (options.onUpdated) options.onUpdated(target.agent);\n else { spawnReplacement(options.dir); setTimeout(() => { void stop().finally(() => process.exit(0)); }, 500); }\n return result;\n }\n result.error = `update to ${target.agent} failed: ${installed.output.slice(-300)}`;\n log(result.error);\n }\n // Hook entries: under \"recommended\" they move to the newest hook named or carried; under \"hold\"\n // only broken ones are repaired, at the version they already name or the one the agent carries.\n const hookTarget = target?.policy === \"recommended\" && target.hook && compareVersions(target.hook, hookVersion()) > 0 ? target.hook : hookVersion();\n result.hookEntries = maintainHookEntries(harnesses, hookTarget, target?.policy !== \"hold\");\n for (const change of result.hookEntries) log(`${change.reason}: ${change.file}`);\n if (connection && (forceSelfCheck || Date.now() - lastSelfCheckAt >= SELF_CHECK_EVERY_MS)) {\n result.selfCheck = await runSelfCheck(options.dir, connection, harnesses, current, { fetchImpl: options.fetchImpl });\n lastSelfCheckAt = Date.now();\n log(`self-check ${result.selfCheck?.ok ? \"passed\" : `failed: ${result.selfCheck?.failed.join(\", \")}`}`);\n }\n } catch (error) { result.error = (error as Error).message; }\n lastMaintenance = result;\n return result;\n };\n\n const schedule = () => {\n if (stopped) return;\n const delay = failures ? Math.min(MAX_BACKOFF_MS, interval * 2 ** Math.min(failures, 6)) : interval;\n timer = setTimeout(() => { void cycle().finally(schedule); }, delay);\n };\n\n const control = await startControl(options.dir, `${AGENT_VERSION} hook/${hookVersion()}`, {\n \"GET /status\": () => ({ ...computerStatus(options.dir), agent: { pid: process.pid, version: AGENT_VERSION, last_cycle: last, last_maintenance: lastMaintenance } }),\n \"POST /flush\": async () => ({ cycle: await cycle() }),\n \"POST /repair\": () => {\n const repaired = repairHookEntries();\n for (const r of repaired) log(`repaired the Scopebond hook entry in ${r.file}`);\n return { repaired };\n },\n \"POST /maintain\": async () => ({ maintenance: await maintain(true) }),\n });\n const stop = async () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n if (maintenanceTimer) clearInterval(maintenanceTimer);\n await running?.catch(() => undefined);\n await control.close();\n };\n log(`Scopebond Agent ${agentVersion()} running for ${options.dir} (control on 127.0.0.1:${control.endpoint.port})`);\n await cycle();\n schedule();\n if (options.maintenance !== false) {\n // Shortly after start (so a fresh sign-in is not slowed), then every six hours.\n setTimeout(() => { if (!stopped) void maintain(); }, 2 * 60_000).unref?.();\n maintenanceTimer = setInterval(() => { if (!stopped) void maintain(); }, UPDATE_EVERY_MS);\n maintenanceTimer.unref?.();\n }\n return { port: control.endpoint.port, cycleNow: cycle, maintainNow: () => maintain(true), stop };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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","sourcesContent":["// The resident loop: one cycle now, then one every interval, sooner after a change is asked for,\n// backing off while the workspace is unreachable. Exactly one agent serves a Scopebond home: a\n// second one finds the first answering and exits. Every six hours it asks the workspace which\n// versions to run, and once a day it runs the end-to-end self-check.\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { hookCommand, hookVersion, loadConnection, userHarnessFile, writeHarnessConfig, type Harness } from \"@scopebond/hook\";\nimport { computerStatus, expectedHarnesses, missingHookEntries, runCycle, type CycleResult } from \"./agent.js\";\nimport { launcherPath } from \"./autostart.js\";\nimport { callAgent, startControl } from \"./ipc.js\";\nimport { runSelfCheck } from \"./selfcheck.js\";\nimport { flushReasons, queueReason } from \"./override-reasons.js\";\nimport { parseQuestion, serialized, systemPrompter, type Prompter } from \"./prompt.js\";\nimport { healthOf, type HealthLevel } from \"./health.js\";\nimport { notifyChange, startTray } from \"./tray.js\";\nimport { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries } from \"./update.js\";\n\nexport const AGENT_VERSION = `agent/${agentVersion()}`;\nconst INTERVAL_MS = 60_000;\nconst MAX_BACKOFF_MS = 15 * 60_000;\nconst UPDATE_EVERY_MS = 6 * 60 * 60 * 1000;\nconst SELF_CHECK_EVERY_MS = 24 * 60 * 60 * 1000;\nexport const AFTER_PID_ENV = \"SCOPEBOND_AGENT_AFTER_PID\";\n\nexport interface ServiceOptions {\n dir: string; intervalMs?: number; fetchImpl?: typeof fetch; log?: (line: string) => void;\n /** Off in tests: the update check and self-check run only when asked. */\n maintenance?: boolean;\n /** Called after a successful update instead of restarting (tests). */\n onUpdated?: (version: string) => void;\n /** Shows the Scopebond window for an override (tests replace it). */\n prompter?: Prompter;\n /** The Windows tray icon (default on Windows; SCOPEBOND_AGENT_TRAY=off turns it off). */\n tray?: boolean;\n}\n\nexport interface Service {\n stop(): Promise<void>;\n cycleNow(): Promise<CycleResult>;\n maintainNow(): Promise<MaintenanceResult>;\n port: number;\n}\n\nexport interface MaintenanceResult {\n at: number;\n policy: \"recommended\" | \"hold\" | \"unknown\";\n updatedTo: string | null;\n hookEntries: Array<{ harness: Harness; file: string; reason: string }>;\n selfCheck: Awaited<ReturnType<typeof runSelfCheck>>;\n error: string | null;\n}\n\n/** Put the Scopebond hook back into agent settings that lost it. A visible change: each one is\n * logged and returned, and only settings of agents this computer has are touched. */\nexport function repairHookEntries(harnesses: Harness[] = expectedHarnesses()): Array<{ harness: Harness; file: string }> {\n const repaired: Array<{ harness: Harness; file: string }> = [];\n for (const harness of missingHookEntries(harnesses)) {\n const file = writeHarnessConfig(userHarnessFile(harness), harness, hookCommand(harness));\n repaired.push({ harness, file });\n }\n return repaired;\n}\n\n/** Start a fresh agent (the updated one) that waits for this process to exit, through the\n * autostart launcher when there is one so it finds Node the same way sign-in does. */\nexport function spawnReplacement(dir: string): void {\n const launcher = launcherPath(dir);\n const cli = realpathSync(fileURLToPath(new URL(\"./cli.js\", import.meta.url)));\n const env = { ...process.env, [AFTER_PID_ENV]: String(process.pid) };\n const [command, args] = existsSync(launcher)\n ? process.platform === \"win32\" ? [\"cmd.exe\", [\"/d\", \"/c\", launcher]] as const : [\"/bin/sh\", [launcher]] as const\n : [process.execPath, [cli, \"run\"]] as const;\n const child = spawn(command, [...args], { env, detached: true, stdio: \"ignore\", windowsHide: true });\n child.unref();\n}\n\nexport async function startService(options: ServiceOptions): Promise<Service> {\n const log = options.log ?? ((line: string) => console.log(`${new Date().toISOString()} ${line}`));\n if (await callAgent(options.dir, \"GET\", \"/status\", undefined, 2_000)) throw new Error(\"a Scopebond Agent is already running for this computer\");\n const interval = options.intervalMs ?? INTERVAL_MS;\n let last: CycleResult | null = null;\n let lastMaintenance: MaintenanceResult | null = null;\n let failures = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let maintenanceTimer: ReturnType<typeof setInterval> | null = null;\n let running: Promise<CycleResult> | null = null;\n let stopped = false;\n let lastSelfCheckAt = 0;\n\n const prompt = serialized(options.prompter ?? systemPrompter);\n let shownLevel: HealthLevel | null = null;\n const health = () => healthOf(computerStatus(options.dir), lastMaintenance?.selfCheck ?? null);\n const noticeHealth = () => { const h = health(); notifyChange(shownLevel, h); shownLevel = h.level; };\n const sendReasons = async () => {\n const connection = loadConnection(options.dir);\n if (!connection) return;\n try { const sent = await flushReasons(options.dir, connection, options.fetchImpl); if (sent) log(`sent ${sent} override reason(s) to the workspace`); }\n catch { /* they wait for the next cycle */ }\n };\n const cycle = async (): Promise<CycleResult> => {\n if (running) return running;\n running = runCycle({ dir: options.dir, fetchImpl: options.fetchImpl }).then(async (result) => {\n await sendReasons();\n try { noticeHealth(); } catch { /* status is best effort */ }\n last = result;\n failures = result.deliveryError ? failures + 1 : 0;\n if (result.delivered) log(`delivered ${result.delivered} record(s); ${result.pending} waiting`);\n if (result.deliveryError) log(`delivery problem: ${result.deliveryError}`);\n return result;\n }).finally(() => { running = null; });\n return running;\n };\n\n const maintain = async (forceSelfCheck = false): Promise<MaintenanceResult> => {\n const result: MaintenanceResult = { at: Date.now(), policy: \"unknown\", updatedTo: null, hookEntries: [], selfCheck: null, error: null };\n try {\n const connection = loadConnection(options.dir);\n const harnesses = expectedHarnesses();\n const target = connection ? await fetchClientVersion(connection, options.fetchImpl) : null;\n if (target) result.policy = target.policy;\n const current = agentVersion();\n if (target?.policy === \"recommended\" && target.agent && compareVersions(target.agent, current) > 0) {\n log(`updating the Scopebond Agent ${current} -> ${target.agent}`);\n const installed = await installAgent(target.agent);\n if (installed.ok) {\n result.updatedTo = target.agent;\n lastMaintenance = result;\n if (options.onUpdated) options.onUpdated(target.agent);\n else { spawnReplacement(options.dir); setTimeout(() => { void stop().finally(() => process.exit(0)); }, 500); }\n return result;\n }\n result.error = `update to ${target.agent} failed: ${installed.output.slice(-300)}`;\n log(result.error);\n }\n // Hook entries: under \"recommended\" they move to the newest hook named or carried; under \"hold\"\n // only broken ones are repaired, at the version they already name or the one the agent carries.\n const hookTarget = target?.policy === \"recommended\" && target.hook && compareVersions(target.hook, hookVersion()) > 0 ? target.hook : hookVersion();\n result.hookEntries = maintainHookEntries(harnesses, hookTarget, target?.policy !== \"hold\");\n for (const change of result.hookEntries) log(`${change.reason}: ${change.file}`);\n if (connection && (forceSelfCheck || Date.now() - lastSelfCheckAt >= SELF_CHECK_EVERY_MS)) {\n result.selfCheck = await runSelfCheck(options.dir, connection, harnesses, current, { fetchImpl: options.fetchImpl });\n lastSelfCheckAt = Date.now();\n log(`self-check ${result.selfCheck?.ok ? \"passed\" : `failed: ${result.selfCheck?.failed.join(\", \") ?? \"no result\"}`}`);\n }\n } catch (error) { result.error = (error as Error).message; }\n lastMaintenance = result;\n return result;\n };\n\n const schedule = () => {\n if (stopped) return;\n const delay = failures ? Math.min(MAX_BACKOFF_MS, interval * 2 ** Math.min(failures, 6)) : interval;\n timer = setTimeout(() => { void cycle().finally(schedule); }, delay);\n };\n\n const control = await startControl(options.dir, `${AGENT_VERSION} hook/${hookVersion()}`, {\n \"GET /status\": () => ({ ...computerStatus(options.dir), health: health(), agent: { pid: process.pid, version: AGENT_VERSION, last_cycle: last, last_maintenance: lastMaintenance } }),\n \"POST /flush\": async () => ({ cycle: await cycle() }),\n \"POST /repair\": () => {\n const repaired = repairHookEntries();\n for (const r of repaired) log(`repaired the Scopebond hook entry in ${r.file}`);\n return { repaired };\n },\n \"POST /maintain\": async () => ({ maintenance: await maintain(true) }),\n // Warn mode: the hook asks; only the window answers. The caller learns the answer, never decides it.\n \"POST /override\": async (body) => {\n const question = parseQuestion(body);\n if (!question) return { decision: \"unavailable\" };\n log(`asking whether to allow \"${question.title}\"`);\n const answer = await prompt(question);\n log(`override ${answer.decision === \"allow\" ? \"given\" : answer.decision === \"deny\" ? \"not given\" : \"not available\"} for \"${question.title}\"`);\n if (answer.decision === \"allow\" && answer.reason) { queueReason(options.dir, question.action_id, answer.reason); void sendReasons(); }\n return answer;\n },\n });\n const tray = (options.tray ?? true) && process.env.SCOPEBOND_AGENT_TRAY !== \"off\" ? startTray(options.dir) : null;\n const stop = async () => {\n stopped = true;\n try { tray?.kill(); } catch { /* already gone */ }\n if (timer) clearTimeout(timer);\n if (maintenanceTimer) clearInterval(maintenanceTimer);\n await running?.catch(() => undefined);\n await control.close();\n };\n log(`Scopebond Agent ${agentVersion()} running for ${options.dir} (control on 127.0.0.1:${control.endpoint.port})`);\n await cycle();\n schedule();\n if (options.maintenance !== false) {\n // Shortly after start (so a fresh sign-in is not slowed), then every six hours.\n setTimeout(() => { if (!stopped) void maintain(); }, 2 * 60_000).unref?.();\n maintenanceTimer = setInterval(() => { if (!stopped) void maintain(); }, UPDATE_EVERY_MS);\n maintenanceTimer.unref?.();\n }\n return { port: control.endpoint.port, cycleNow: cycle, maintainNow: () => maintain(true), stop };\n}\n"]}
|
package/dist/tray.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import type { Health, HealthLevel } from "./health.js";
|
|
3
|
+
/** The tray script. The Scopebond home and the agent's process id reach it as base64 data, never as code. */
|
|
4
|
+
export declare function trayScript(home: string, agentPid: number): string;
|
|
5
|
+
/** Start the tray icon for this agent (Windows only). Returns the process so the agent can close it when it stops. */
|
|
6
|
+
export declare function startTray(home: string, agentPid?: number): ChildProcess | null;
|
|
7
|
+
/** macOS and Linux: notify when the state gets worse, and once when it is green again. Returns whether it notified. */
|
|
8
|
+
export declare function notifyChange(previous: HealthLevel | null, health: Health, run?: typeof spawn): boolean;
|
|
9
|
+
//# sourceMappingURL=tray.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tray.d.ts","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAIvD,6GAA6G;AAC7G,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6DjE;AAED,sHAAsH;AACtH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAc,GAAG,YAAY,GAAG,IAAI,CAQnF;AAID,uHAAuH;AACvH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAE,OAAO,KAAa,GAAG,OAAO,CAW7G"}
|
package/dist/tray.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Where a person sees the agent. Windows: a tray icon from Windows' own signed PowerShell (no extra program to install or
|
|
2
|
+
// sign): a green, amber or red dot, the headline as its tooltip, a menu with the one fix, and a balloon when it turns amber or
|
|
3
|
+
// red. It asks the agent's local channel every 30 seconds and closes itself when the agent stops. macOS and Linux: a system
|
|
4
|
+
// notification when the state gets worse, and once more when it recovers.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { AGENT_FILE, TOKEN_HEADER } from "./ipc.js";
|
|
7
|
+
const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
8
|
+
/** The tray script. The Scopebond home and the agent's process id reach it as base64 data, never as code. */
|
|
9
|
+
export function trayScript(home, agentPid) {
|
|
10
|
+
return `
|
|
11
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
12
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
13
|
+
Add-Type -AssemblyName System.Drawing
|
|
14
|
+
function T($s) { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)) }
|
|
15
|
+
$endpointFile = Join-Path (T '${b64(home)}') '${AGENT_FILE}'
|
|
16
|
+
$agentPid = ${Math.trunc(agentPid)}
|
|
17
|
+
function Dot($color) {
|
|
18
|
+
$b = New-Object Drawing.Bitmap 16, 16
|
|
19
|
+
$g = [Drawing.Graphics]::FromImage($b)
|
|
20
|
+
$g.SmoothingMode = 'AntiAlias'
|
|
21
|
+
$g.FillEllipse((New-Object Drawing.SolidBrush $color), 2, 2, 12, 12)
|
|
22
|
+
$g.Dispose()
|
|
23
|
+
[Drawing.Icon]::FromHandle($b.GetHicon())
|
|
24
|
+
}
|
|
25
|
+
$icons = @{ green = (Dot ([Drawing.Color]::FromArgb(46, 160, 67))); amber = (Dot ([Drawing.Color]::FromArgb(219, 154, 4))); red = (Dot ([Drawing.Color]::FromArgb(207, 34, 46))) }
|
|
26
|
+
$tray = New-Object Windows.Forms.NotifyIcon
|
|
27
|
+
$tray.Icon = $icons.amber
|
|
28
|
+
$tray.Text = 'Scopebond'
|
|
29
|
+
$tray.Visible = $true
|
|
30
|
+
$menu = New-Object Windows.Forms.ContextMenuStrip
|
|
31
|
+
$tray.ContextMenuStrip = $menu
|
|
32
|
+
$script:level = ''
|
|
33
|
+
$script:fix = $null
|
|
34
|
+
function Call($method, $route) {
|
|
35
|
+
$e = Get-Content -Raw $endpointFile | ConvertFrom-Json
|
|
36
|
+
$uri = 'http://127.0.0.1:' + $e.port + $route
|
|
37
|
+
$headers = @{ '${TOKEN_HEADER}' = $e.token }
|
|
38
|
+
if ($method -eq 'POST') { Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType 'application/json' -Body '{}' -TimeoutSec 300 }
|
|
39
|
+
else { Invoke-RestMethod -Method Get -Uri $uri -Headers $headers -TimeoutSec 10 }
|
|
40
|
+
}
|
|
41
|
+
function Refresh {
|
|
42
|
+
if (-not (Get-Process -Id $agentPid)) { $tray.Visible = $false; [Windows.Forms.Application]::Exit(); return }
|
|
43
|
+
try { $h = (Call 'GET' '/status').health } catch { $h = $null }
|
|
44
|
+
if (-not $h) { $h = @{ level = 'red'; headline = 'The Scopebond Agent is not answering'; fix = $null; hint = $null } }
|
|
45
|
+
$tray.Icon = $icons[$h.level]
|
|
46
|
+
$tip = 'Scopebond: ' + $h.headline
|
|
47
|
+
$tray.Text = $(if ($tip.Length -gt 63) { $tip.Substring(0, 60) + '...' } else { $tip })
|
|
48
|
+
$menu.Items.Clear()
|
|
49
|
+
$head = $menu.Items.Add($h.headline); $head.Enabled = $false
|
|
50
|
+
if ($h.hint) { $hint = $menu.Items.Add($h.hint); $hint.Enabled = $false }
|
|
51
|
+
if ($h.fix) {
|
|
52
|
+
$route = $h.fix.route
|
|
53
|
+
$item = $menu.Items.Add($h.fix.label)
|
|
54
|
+
$item.Add_Click({ try { Call 'POST' $route | Out-Null } catch {} ; Refresh }.GetNewClosure())
|
|
55
|
+
}
|
|
56
|
+
[void]$menu.Items.Add('-')
|
|
57
|
+
$check = $menu.Items.Add('Check now'); $check.Add_Click({ try { Call 'POST' '/maintain' | Out-Null } catch {} ; Refresh })
|
|
58
|
+
$quit = $menu.Items.Add('Hide this icon'); $quit.Add_Click({ $tray.Visible = $false; [Windows.Forms.Application]::Exit() })
|
|
59
|
+
if ($script:level -ne $h.level -and $h.level -ne 'green' -and $script:level -ne '') { $tray.ShowBalloonTip(8000, 'Scopebond', $h.headline, $(if ($h.level -eq 'red') { 'Error' } else { 'Warning' })) }
|
|
60
|
+
$script:level = $h.level
|
|
61
|
+
}
|
|
62
|
+
$timer = New-Object Windows.Forms.Timer
|
|
63
|
+
$timer.Interval = 30000
|
|
64
|
+
$timer.Add_Tick({ Refresh })
|
|
65
|
+
$timer.Start()
|
|
66
|
+
Refresh
|
|
67
|
+
[Windows.Forms.Application]::Run()
|
|
68
|
+
$tray.Dispose()
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
/** Start the tray icon for this agent (Windows only). Returns the process so the agent can close it when it stops. */
|
|
72
|
+
export function startTray(home, agentPid = process.pid) {
|
|
73
|
+
if (process.platform !== "win32")
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const encoded = Buffer.from(trayScript(home, agentPid), "utf16le").toString("base64");
|
|
77
|
+
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-STA", "-WindowStyle", "Hidden", "-EncodedCommand", encoded], { stdio: "ignore", windowsHide: true });
|
|
78
|
+
child.on("error", () => { });
|
|
79
|
+
return child;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const RANK = { green: 0, amber: 1, red: 2 };
|
|
86
|
+
/** macOS and Linux: notify when the state gets worse, and once when it is green again. Returns whether it notified. */
|
|
87
|
+
export function notifyChange(previous, health, run = spawn) {
|
|
88
|
+
if (process.platform === "win32" || previous === null || previous === health.level)
|
|
89
|
+
return false;
|
|
90
|
+
if (RANK[health.level] < RANK[previous] && health.level !== "green")
|
|
91
|
+
return false;
|
|
92
|
+
const text = health.level === "green" ? "Working again: " + health.headline : health.headline + (health.hint ? `. ${health.hint}` : "");
|
|
93
|
+
try {
|
|
94
|
+
const child = process.platform === "darwin"
|
|
95
|
+
? run("osascript", ["-e", "on run argv", "-e", "display notification (item 2 of argv) with title (item 1 of argv)", "-e", "end run", "Scopebond", text], { stdio: "ignore" })
|
|
96
|
+
: run("notify-send", ["Scopebond", text], { stdio: "ignore" });
|
|
97
|
+
child.on("error", () => { });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=tray.js.map
|
package/dist/tray.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tray.js","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"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","sourcesContent":["// Where a person sees the agent. Windows: a tray icon from Windows' own signed PowerShell (no extra program to install or\n// sign): a green, amber or red dot, the headline as its tooltip, a menu with the one fix, and a balloon when it turns amber or\n// red. It asks the agent's local channel every 30 seconds and closes itself when the agent stops. macOS and Linux: a system\n// notification when the state gets worse, and once more when it recovers.\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { AGENT_FILE, TOKEN_HEADER } from \"./ipc.js\";\nimport type { Health, HealthLevel } from \"./health.js\";\n\nconst b64 = (s: string) => Buffer.from(s, \"utf8\").toString(\"base64\");\n\n/** The tray script. The Scopebond home and the agent's process id reach it as base64 data, never as code. */\nexport function trayScript(home: string, agentPid: number): string {\n return `\n$ErrorActionPreference = 'SilentlyContinue'\nAdd-Type -AssemblyName System.Windows.Forms\nAdd-Type -AssemblyName System.Drawing\nfunction T($s) { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)) }\n$endpointFile = Join-Path (T '${b64(home)}') '${AGENT_FILE}'\n$agentPid = ${Math.trunc(agentPid)}\nfunction Dot($color) {\n $b = New-Object Drawing.Bitmap 16, 16\n $g = [Drawing.Graphics]::FromImage($b)\n $g.SmoothingMode = 'AntiAlias'\n $g.FillEllipse((New-Object Drawing.SolidBrush $color), 2, 2, 12, 12)\n $g.Dispose()\n [Drawing.Icon]::FromHandle($b.GetHicon())\n}\n$icons = @{ green = (Dot ([Drawing.Color]::FromArgb(46, 160, 67))); amber = (Dot ([Drawing.Color]::FromArgb(219, 154, 4))); red = (Dot ([Drawing.Color]::FromArgb(207, 34, 46))) }\n$tray = New-Object Windows.Forms.NotifyIcon\n$tray.Icon = $icons.amber\n$tray.Text = 'Scopebond'\n$tray.Visible = $true\n$menu = New-Object Windows.Forms.ContextMenuStrip\n$tray.ContextMenuStrip = $menu\n$script:level = ''\n$script:fix = $null\nfunction Call($method, $route) {\n $e = Get-Content -Raw $endpointFile | ConvertFrom-Json\n $uri = 'http://127.0.0.1:' + $e.port + $route\n $headers = @{ '${TOKEN_HEADER}' = $e.token }\n if ($method -eq 'POST') { Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType 'application/json' -Body '{}' -TimeoutSec 300 }\n else { Invoke-RestMethod -Method Get -Uri $uri -Headers $headers -TimeoutSec 10 }\n}\nfunction Refresh {\n if (-not (Get-Process -Id $agentPid)) { $tray.Visible = $false; [Windows.Forms.Application]::Exit(); return }\n try { $h = (Call 'GET' '/status').health } catch { $h = $null }\n if (-not $h) { $h = @{ level = 'red'; headline = 'The Scopebond Agent is not answering'; fix = $null; hint = $null } }\n $tray.Icon = $icons[$h.level]\n $tip = 'Scopebond: ' + $h.headline\n $tray.Text = $(if ($tip.Length -gt 63) { $tip.Substring(0, 60) + '...' } else { $tip })\n $menu.Items.Clear()\n $head = $menu.Items.Add($h.headline); $head.Enabled = $false\n if ($h.hint) { $hint = $menu.Items.Add($h.hint); $hint.Enabled = $false }\n if ($h.fix) {\n $route = $h.fix.route\n $item = $menu.Items.Add($h.fix.label)\n $item.Add_Click({ try { Call 'POST' $route | Out-Null } catch {} ; Refresh }.GetNewClosure())\n }\n [void]$menu.Items.Add('-')\n $check = $menu.Items.Add('Check now'); $check.Add_Click({ try { Call 'POST' '/maintain' | Out-Null } catch {} ; Refresh })\n $quit = $menu.Items.Add('Hide this icon'); $quit.Add_Click({ $tray.Visible = $false; [Windows.Forms.Application]::Exit() })\n if ($script:level -ne $h.level -and $h.level -ne 'green' -and $script:level -ne '') { $tray.ShowBalloonTip(8000, 'Scopebond', $h.headline, $(if ($h.level -eq 'red') { 'Error' } else { 'Warning' })) }\n $script:level = $h.level\n}\n$timer = New-Object Windows.Forms.Timer\n$timer.Interval = 30000\n$timer.Add_Tick({ Refresh })\n$timer.Start()\nRefresh\n[Windows.Forms.Application]::Run()\n$tray.Dispose()\n`;\n}\n\n/** Start the tray icon for this agent (Windows only). Returns the process so the agent can close it when it stops. */\nexport function startTray(home: string, agentPid = process.pid): ChildProcess | null {\n if (process.platform !== \"win32\") return null;\n try {\n const encoded = Buffer.from(trayScript(home, agentPid), \"utf16le\").toString(\"base64\");\n const child = spawn(\"powershell.exe\", [\"-NoProfile\", \"-NonInteractive\", \"-STA\", \"-WindowStyle\", \"Hidden\", \"-EncodedCommand\", encoded], { stdio: \"ignore\", windowsHide: true });\n child.on(\"error\", () => { /* no PowerShell, or blocked: the agent keeps working without an icon */ });\n return child;\n } catch { return null; }\n}\n\nconst RANK: Record<HealthLevel, number> = { green: 0, amber: 1, red: 2 };\n\n/** macOS and Linux: notify when the state gets worse, and once when it is green again. Returns whether it notified. */\nexport function notifyChange(previous: HealthLevel | null, health: Health, run: typeof spawn = spawn): boolean {\n if (process.platform === \"win32\" || previous === null || previous === health.level) return false;\n if (RANK[health.level] < RANK[previous] && health.level !== \"green\") return false;\n const text = health.level === \"green\" ? \"Working again: \" + health.headline : health.headline + (health.hint ? `. ${health.hint}` : \"\");\n try {\n const child = process.platform === \"darwin\"\n ? run(\"osascript\", [\"-e\", \"on run argv\", \"-e\", \"display notification (item 2 of argv) with title (item 1 of argv)\", \"-e\", \"end run\", \"Scopebond\", text], { stdio: \"ignore\" })\n : run(\"notify-send\", [\"Scopebond\", text], { stdio: \"ignore\" });\n child.on(\"error\", () => { /* no notification service */ });\n return true;\n } catch { return false; }\n}\n\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scopebond/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "The Scopebond Agent — one resident process per user that delivers this computer's signed records to its Scopebond workspace, keeps its rules and connection current, and reports its health, so nobody has to run a command to keep it working.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Avouro LLC",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"dist"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@scopebond/gateway": "0.
|
|
43
|
-
"@scopebond/hook": "0.
|
|
44
|
-
"@scopebond/sdk": "0.1.
|
|
42
|
+
"@scopebond/gateway": "0.13.0",
|
|
43
|
+
"@scopebond/hook": "0.16.0",
|
|
44
|
+
"@scopebond/sdk": "0.1.4"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|