@timqi/pier 0.0.29 → 0.1.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 +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/dist/secrets.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
// Layer-1 secret encryption
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// raw key in the file (the no-vt fallback — same at-rest level as the
|
|
8
|
-
// plaintext files it replaces, and the mode is the operator's explicit
|
|
9
|
-
// choice, never a silent downgrade).
|
|
10
|
-
//
|
|
11
|
-
// Both keys live in `master.key` (JSON: kek, wrapped dek, dek id), not the
|
|
12
|
-
// database: rotation is then a single atomic rename, with no crash window
|
|
13
|
-
// where the file holds the new KEK and the database a DEK wrapped by the old
|
|
14
|
-
// one. Layer 2 — secrets needing per-use approval — never passes through
|
|
15
|
-
// here: those stay `vt://` strings Pier cannot read, and the agent runs vt
|
|
16
|
-
// itself.
|
|
1
|
+
// Layer-1 secret encryption for the credentials Pier must read by itself.
|
|
2
|
+
// Standard envelope: a KEK from `master.key` (a `vt://` record, or a raw key in
|
|
3
|
+
// file mode — the operator's explicit choice) wraps a DEK, and only the DEK
|
|
4
|
+
// touches data, so rotating the KEK rewrites one file and zero rows. Both keys
|
|
5
|
+
// live in that one file so rotation is a single atomic rename. Layer 2
|
|
6
|
+
// (per-use approval) never passes here: the agent runs vt itself.
|
|
17
7
|
import { spawn } from "node:child_process";
|
|
18
8
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
19
9
|
import { chmodSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -22,6 +12,9 @@ import { pierPath } from "./paths.js";
|
|
|
22
12
|
const log = logger("secrets");
|
|
23
13
|
const KEY_BYTES = 32; // AES-256
|
|
24
14
|
const IV_BYTES = 12; // GCM standard nonce
|
|
15
|
+
/** Anything not matching predates sealing: honored as plaintext, and re-sealed
|
|
16
|
+
* by whichever store owns the row. */
|
|
17
|
+
export const isSealed = (blob) => /^v1:[0-9a-f]{8}:/.test(blob);
|
|
25
18
|
export class Secrets {
|
|
26
19
|
path;
|
|
27
20
|
vt;
|
|
@@ -39,18 +32,12 @@ export class Secrets {
|
|
|
39
32
|
get mode() {
|
|
40
33
|
return this.#file ? (this.#file.kek.startsWith("vt://") ? "vt" : "file") : undefined;
|
|
41
34
|
}
|
|
42
|
-
/** Why decrypt is refused right now — "" once unlocked. Shown in the
|
|
43
|
-
* Console's Security tab, which is where a refused unlock gets repaired. */
|
|
44
35
|
get lockedReason() {
|
|
45
36
|
return this.#lockedReason;
|
|
46
37
|
}
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* failed vt approval or a corrupt file, and remembers why: the process must
|
|
51
|
-
* keep serving (web is how the operator unlocks or repairs), but every
|
|
52
|
-
* refused decrypt names the reason instead of pretending to be empty.
|
|
53
|
-
*/
|
|
38
|
+
/** Created on first boot in file mode, so an unattended start needs no
|
|
39
|
+
* ceremony. Throws and remembers why: the process must keep serving (web is
|
|
40
|
+
* how the operator repairs), and every refused decrypt names the reason. */
|
|
54
41
|
async unlock() {
|
|
55
42
|
try {
|
|
56
43
|
let raw;
|
|
@@ -58,9 +45,8 @@ export class Secrets {
|
|
|
58
45
|
raw = readFileSync(this.path, "utf8");
|
|
59
46
|
}
|
|
60
47
|
catch (err) {
|
|
61
|
-
// Only
|
|
62
|
-
//
|
|
63
|
-
// fresh key over the existing one and destroy every sealed credential.
|
|
48
|
+
// Only ENOENT is first boot: any other error falling through to
|
|
49
|
+
// #create() would rename a fresh key over the existing one.
|
|
64
50
|
if (err.code !== "ENOENT")
|
|
65
51
|
throw err;
|
|
66
52
|
this.#file = this.#create();
|
|
@@ -98,12 +84,8 @@ export class Secrets {
|
|
|
98
84
|
throw new Error(`sealed by unknown key ${dekId}, have ${file.dekId}`);
|
|
99
85
|
return open(dek, rest.join(":"), `v1:${dekId}`).toString("utf8");
|
|
100
86
|
}
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
* the new KEK is protected (entering vt mode runs `vt create`, one
|
|
104
|
-
* approval); omitted, the current mode is kept. The rewrapped file lands by
|
|
105
|
-
* atomic rename — a crash leaves either the old working file or the new one.
|
|
106
|
-
*/
|
|
87
|
+
/** New KEK, same DEK: every stored envelope stays valid. Omitted `mode`
|
|
88
|
+
* keeps the current one. */
|
|
107
89
|
async rotateKek(mode = this.mode ?? "file") {
|
|
108
90
|
const { file } = this.#unlocked();
|
|
109
91
|
const kek = randomBytes(KEY_BYTES);
|
|
@@ -119,16 +101,10 @@ export class Secrets {
|
|
|
119
101
|
this.#file = next;
|
|
120
102
|
log.info(`KEK rotated (${mode} mode, dek ${file.dekId} unchanged)`);
|
|
121
103
|
}
|
|
122
|
-
/**
|
|
123
|
-
* What vt says about itself. A refused or impossible unlock is almost never
|
|
124
|
-
* Pier's fault — missing binary, no agent listening, config pointing
|
|
125
|
-
* elsewhere — and `lockedReason` only carries the last error. Read-only, so
|
|
126
|
-
* it is safe to run while locked; vt's own report is the repair instruction.
|
|
127
|
-
*/
|
|
104
|
+
/** Read-only, safe while locked; vt's own report is the repair instruction. */
|
|
128
105
|
doctor() {
|
|
129
106
|
return this.vt.doctor();
|
|
130
107
|
}
|
|
131
|
-
/** First boot: random KEK and DEK, file mode. */
|
|
132
108
|
#create() {
|
|
133
109
|
const kek = randomBytes(KEY_BYTES);
|
|
134
110
|
const dekId = randomBytes(4).toString("hex");
|
|
@@ -170,9 +146,7 @@ function open(key, sealed, aad) {
|
|
|
170
146
|
decipher.setAuthTag(tag);
|
|
171
147
|
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
172
148
|
}
|
|
173
|
-
|
|
174
|
-
* spawn/exit error — unlock() records it and the operator reads it. */
|
|
175
|
-
export const vtCli = {
|
|
149
|
+
const vtCli = {
|
|
176
150
|
read: (record) => run("vt", ["read", record]),
|
|
177
151
|
create: async (plaintext) => {
|
|
178
152
|
const out = await run("vt", ["create"], plaintext);
|
|
@@ -181,8 +155,7 @@ export const vtCli = {
|
|
|
181
155
|
throw new Error("vt create printed no vt:// record");
|
|
182
156
|
return record;
|
|
183
157
|
},
|
|
184
|
-
//
|
|
185
|
-
// a hung probe would leave the Console waiting on a diagnosis forever.
|
|
158
|
+
// doctor probes over the network; a hung probe would hang the Console.
|
|
186
159
|
doctor: () => run("vt", ["doctor"], undefined, 15_000),
|
|
187
160
|
};
|
|
188
161
|
function run(cmd, args, stdin, timeoutMs) {
|
package/dist/service.js
CHANGED
|
@@ -1,32 +1,22 @@
|
|
|
1
|
-
// The systemd user unit Pier writes for itself
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// dedicated service user it would be an agent that cannot touch the files you
|
|
6
|
-
// wanted it to work on.
|
|
7
|
-
//
|
|
8
|
-
// Writing this file is a command rather than a page of documentation to copy
|
|
9
|
-
// because two of its lines are only knowable at runtime: the absolute path of
|
|
10
|
-
// the node that is running (systemd starts with a minimal PATH, so a node
|
|
11
|
-
// installed by fnm/nvm/asdf is not on it) and the absolute path of the
|
|
12
|
-
// installed entry point.
|
|
1
|
+
// The systemd *user* unit Pier writes for itself: Pier runs as you, so it can
|
|
2
|
+
// touch the files you wanted it to work on. Written by a command because two
|
|
3
|
+
// lines are only knowable at runtime — the absolute node (systemd's minimal
|
|
4
|
+
// PATH has no fnm/nvm node) and the installed entry point.
|
|
13
5
|
import { execFileSync } from "node:child_process";
|
|
14
6
|
import { existsSync, mkdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
15
7
|
import { homedir, userInfo } from "node:os";
|
|
16
8
|
import { dirname, join } from "node:path";
|
|
17
9
|
export const UNIT_NAME = "pier.service";
|
|
18
|
-
|
|
19
|
-
/** `~/.config/systemd/user/pier.service` — where a user unit belongs. */
|
|
10
|
+
const UPDATE_UNIT_NAME = "pier-update.service";
|
|
20
11
|
export const unitPath = (home = homedir()) => join(home, ".config", "systemd", "user", UNIT_NAME);
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
12
|
+
/** Written once and never touched again: what the unit may consume is the
|
|
13
|
+
* operator's call. */
|
|
23
14
|
export const limitsPath = (home = homedir()) => join(dirname(unitPath(home)), `${UNIT_NAME}.d`, "limits.conf");
|
|
24
|
-
/** The oneshot that installs a new version, beside the unit it restarts. */
|
|
25
15
|
export const updateUnitPath = (home = homedir()) => join(dirname(unitPath(home)), UPDATE_UNIT_NAME);
|
|
26
|
-
/**
|
|
16
|
+
/** Regenerated before each updater run. */
|
|
27
17
|
export const updateRuntimePath = (home = homedir()) => join(dirname(unitPath(home)), `${UPDATE_UNIT_NAME}.d`, "runtime.conf");
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
18
|
+
/** Percent is doubled because specifier expansion runs after parsing; dollar
|
|
19
|
+
* only for command lines. */
|
|
30
20
|
function quote(value, command = false) {
|
|
31
21
|
if (/[\0\r\n]/.test(value))
|
|
32
22
|
throw new Error("systemd values cannot contain control characters");
|
|
@@ -36,14 +26,9 @@ function quote(value, command = false) {
|
|
|
36
26
|
return `"${escaped}"`;
|
|
37
27
|
}
|
|
38
28
|
const environment = (key, value) => `Environment=${quote(`${key}=${value}`)}`;
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Recorded at install rather than sourced from a login shell at start, which
|
|
44
|
-
* would hand a dotfile the power to decide whether Pier boots and which node
|
|
45
|
-
* npm installs into. Relative entries are dropped: they would resolve against
|
|
46
|
-
* WorkingDirectory, which is not where the operator was standing. */
|
|
29
|
+
/** Recorded at install rather than sourced from a login shell at start, which
|
|
30
|
+
* would hand a dotfile the power to decide whether Pier boots. Relative
|
|
31
|
+
* entries would resolve against WorkingDirectory, so they are dropped. */
|
|
47
32
|
function pathEnv(execPath, shellPath) {
|
|
48
33
|
const seen = new Set();
|
|
49
34
|
return [dirname(execPath), ...(shellPath ?? "").split(":"), "/usr/local/bin", "/usr/bin", "/bin"]
|
|
@@ -86,8 +71,7 @@ SyslogIdentifier=pier
|
|
|
86
71
|
WantedBy=default.target
|
|
87
72
|
`;
|
|
88
73
|
}
|
|
89
|
-
/**
|
|
90
|
-
* beside its recorded Node; a forced reinstall writes the exact executable. */
|
|
74
|
+
/** A unit that recorded no npm: the bridge assumes npm beside its Node. */
|
|
91
75
|
function legacyOptions(home) {
|
|
92
76
|
const text = readFileSync(unitPath(home), "utf8");
|
|
93
77
|
const start = text.match(/^ExecStart=(\S+) (\S+)$/m);
|
|
@@ -105,14 +89,9 @@ function legacyOptions(home) {
|
|
|
105
89
|
...(pierHome ? { pierHome } : {}),
|
|
106
90
|
};
|
|
107
91
|
}
|
|
108
|
-
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* Both slow steps run while the service is still up, so the downtime is one
|
|
112
|
-
* stop and one start rather than the ~10s an install takes. The snapshot is
|
|
113
|
-
* consistent on a live database — `VACUUM INTO` off a read-only connection
|
|
114
|
-
* (db.ts) — and it still runs from the tree npm is about to replace, so the
|
|
115
|
-
* copy carries the version whose schema it pairs with. */
|
|
92
|
+
/** Snapshot and install run while the service is still up, so downtime is one
|
|
93
|
+
* stop and one start. The snapshot runs from the tree npm is about to replace,
|
|
94
|
+
* so the copy carries the version whose schema it pairs with. */
|
|
116
95
|
export function renderUpdateUnit(options) {
|
|
117
96
|
const { execPath, npmPath, entry, pierHome, shellPath } = options;
|
|
118
97
|
const cli = join(dirname(entry), "cli.js");
|
|
@@ -140,13 +119,9 @@ ExecStart=systemctl --user stop ${UNIT_NAME}
|
|
|
140
119
|
ExecStopPost=systemctl --user start ${UNIT_NAME}
|
|
141
120
|
`;
|
|
142
121
|
}
|
|
143
|
-
/**
|
|
144
|
-
*
|
|
145
|
-
|
|
146
|
-
* exists to protect the OS and sshd outside it. Written commented so the
|
|
147
|
-
* operator tuning it can see what each line buys.
|
|
148
|
-
*/
|
|
149
|
-
export function renderLimits() {
|
|
122
|
+
/** A share of the machine, not "how much Pier needs": the limit covers every
|
|
123
|
+
* command a turn ran, and exists to protect the OS and sshd outside it. */
|
|
124
|
+
function renderLimits() {
|
|
150
125
|
return `[Service]
|
|
151
126
|
# Soft ceiling: past this the kernel reclaims hard and lets the unit crawl
|
|
152
127
|
# instead of killing anything. This is the one that should bite first.
|
|
@@ -164,8 +139,6 @@ OOMScoreAdjust=200
|
|
|
164
139
|
OOMPolicy=continue
|
|
165
140
|
`;
|
|
166
141
|
}
|
|
167
|
-
/** A command failure is printed here and propagated by its caller. Linger is
|
|
168
|
-
* the only deliberately best-effort step. */
|
|
169
142
|
const runner = (say) => (argv) => {
|
|
170
143
|
try {
|
|
171
144
|
execFileSync(argv[0], argv.slice(1), { stdio: "pipe" });
|
|
@@ -202,8 +175,8 @@ export function install(options) {
|
|
|
202
175
|
}
|
|
203
176
|
if (!run(["systemctl", "--user", "daemon-reload"]))
|
|
204
177
|
return false;
|
|
205
|
-
// Without lingering
|
|
206
|
-
//
|
|
178
|
+
// Without lingering the user manager stops at logout, taking every scheduled
|
|
179
|
+
// task with it. It can need a polkit prompt, hence best-effort.
|
|
207
180
|
if (!run(["loginctl", "enable-linger", userInfo().username])) {
|
|
208
181
|
say(` run it yourself so Pier survives logout: loginctl enable-linger ${userInfo().username}`);
|
|
209
182
|
}
|
|
@@ -218,20 +191,13 @@ export function install(options) {
|
|
|
218
191
|
say(`started. The first run prints a password once: journalctl --user -u pier -e`);
|
|
219
192
|
return true;
|
|
220
193
|
}
|
|
221
|
-
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* The absolute node and npm paths in the unit are deliberate — systemd's PATH
|
|
227
|
-
* has neither — but they pin the unit to one directory of one version manager.
|
|
228
|
-
* `fnm install 26 && fnm uninstall 24` leaves ExecStart naming a Node that is
|
|
229
|
-
* gone; the running process survives (Linux keeps a deleted binary mapped),
|
|
230
|
-
* so nothing would notice until the update, or the next boot, failed.
|
|
231
|
-
*/
|
|
194
|
+
/** The absolute node and npm paths pin the unit to one version manager's
|
|
195
|
+
* directory: `fnm uninstall 24` leaves ExecStart naming a Node that is gone,
|
|
196
|
+
* and the running process survives it (Linux keeps a deleted binary mapped),
|
|
197
|
+
* so nothing would notice until the next boot failed. */
|
|
232
198
|
export function updaterProblem(home = homedir()) {
|
|
233
199
|
if (!existsSync(unitPath(home)))
|
|
234
|
-
return null; // not a service install
|
|
200
|
+
return null; // not a service install
|
|
235
201
|
const path = updateUnitPath(home);
|
|
236
202
|
let unit;
|
|
237
203
|
if (existsSync(path)) {
|
|
@@ -243,9 +209,7 @@ export function updaterProblem(home = homedir()) {
|
|
|
243
209
|
}
|
|
244
210
|
}
|
|
245
211
|
else {
|
|
246
|
-
//
|
|
247
|
-
// so a missing file is only a problem when that bridge cannot either —
|
|
248
|
-
// and the generated text gets the same executable check below.
|
|
212
|
+
// startUpdate generates a missing updater from the main unit; check that text.
|
|
249
213
|
try {
|
|
250
214
|
unit = renderUpdateUnit(legacyOptions(home));
|
|
251
215
|
}
|
|
@@ -253,10 +217,8 @@ export function updaterProblem(home = homedir()) {
|
|
|
253
217
|
return `${UPDATE_UNIT_NAME} is missing — run: pier service install --force`;
|
|
254
218
|
}
|
|
255
219
|
}
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// judge; the escaping is undone before existsSync sees a path (a `%` or `$`
|
|
259
|
-
// in it would otherwise read as gone on a working updater).
|
|
220
|
+
// Unparseable means hand-edited, not this function's to judge. Unescaped
|
|
221
|
+
// before existsSync: a `%` or `$` in the path would otherwise read as gone.
|
|
260
222
|
const install = unit.match(/^ExecStart="((?:\\.|[^"\r\n])+)" "((?:\\.|[^"\r\n])+)" install -g/m);
|
|
261
223
|
if (!install)
|
|
262
224
|
return null;
|
|
@@ -278,16 +240,14 @@ function runningPierHome(home) {
|
|
|
278
240
|
?.slice("PIER_HOME=".length);
|
|
279
241
|
return value || join(home, ".pier");
|
|
280
242
|
}
|
|
281
|
-
// Installed but stopped: the unit
|
|
282
|
-
//
|
|
283
|
-
// or the drop-in would carry `%%`/`\\"` into a real path.
|
|
243
|
+
// Installed but stopped: the unit records any override, quoted or bare;
|
|
244
|
+
// undo quote()'s escaping or the drop-in would carry `%%` into a real path.
|
|
284
245
|
const raw = readFileSync(unitPath(home), "utf8")
|
|
285
246
|
.match(/^Environment="?PIER_HOME=((?:\\.|[^"\r\n])+)"?$/m)?.[1];
|
|
286
247
|
const fromUnit = raw?.replaceAll("%%", "%").replace(/\\(.)/g, "$1");
|
|
287
248
|
return fromUnit || join(home, ".pier");
|
|
288
249
|
}
|
|
289
|
-
/**
|
|
290
|
-
* running service's effective home, including an operator override. */
|
|
250
|
+
/** The drop-in captures the running service's effective home, override included. */
|
|
291
251
|
export function startUpdate(options) {
|
|
292
252
|
const { home = homedir(), say } = options;
|
|
293
253
|
const run = options.exec ?? runner(say);
|
|
@@ -334,8 +294,6 @@ export function uninstall(home = homedir(), say = console.log, exec) {
|
|
|
334
294
|
}
|
|
335
295
|
}
|
|
336
296
|
const ok = run(["systemctl", "--user", "daemon-reload"]);
|
|
337
|
-
// Left alone on purpose: the database, the boards, and linger — none of them
|
|
338
|
-
// are this command's to decide about.
|
|
339
297
|
say(`$PIER_HOME is untouched; linger is still enabled.`);
|
|
340
298
|
return ok;
|
|
341
299
|
}
|
package/dist/settings.js
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
// Instance settings: the facts about *this* Pier that are neither a credential
|
|
2
|
-
// nor per-session
|
|
3
|
-
// a Host header is whatever a proxy passed on) and the operator's model menu.
|
|
4
|
-
//
|
|
5
|
-
// A key-value table, so the next setting is not the next table and not a third
|
|
6
|
-
// kind of storage. It used to be a JSON file, justified by "the agent reads it
|
|
7
|
-
// too" — the agent is told the URL in its system prompt (core/reply.ts), and
|
|
8
|
-
// nothing outside this process ever opened that file.
|
|
2
|
+
// nor per-session. A key-value table, so the next setting is not the next table.
|
|
9
3
|
import { isThinkingLevel } from "./core/types.js";
|
|
10
4
|
import { pierDb, transact } from "./db.js";
|
|
11
5
|
import { logger } from "./log.js";
|
|
12
|
-
// The one place the custom-tool vocabulary lives (names, ubix sources, the
|
|
13
|
-
// names Pier already owns). Imported rather than copied: a second validator
|
|
14
|
-
// would be the third-copy bug one release later, and tools.ts is root-layer
|
|
15
|
-
// like this file, so nothing crosses a seam.
|
|
16
6
|
import { normalizeCustomTools } from "./tools.js";
|
|
17
7
|
const log = logger("settings");
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
* mistyped host quietly turned into a URL produces board links that 404 for
|
|
21
|
-
* the person they were sent to, and the sender never finds out.
|
|
22
|
-
*/
|
|
8
|
+
/** `""` clears it, `null` rejects it: a mistyped host quietly turned into a URL
|
|
9
|
+
* produces board links that 404 for the person they were sent to. */
|
|
23
10
|
export function normalizePublicUrl(raw) {
|
|
24
11
|
const text = raw.trim();
|
|
25
12
|
if (!text)
|
|
26
13
|
return "";
|
|
27
14
|
let url;
|
|
28
15
|
try {
|
|
29
|
-
// Scheme-less input is the common way to type a host, and https is the
|
|
30
|
-
// only guess worth making for something on the internet.
|
|
31
16
|
url = new URL(text.includes("://") ? text : `https://${text}`);
|
|
32
17
|
}
|
|
33
18
|
catch {
|
|
@@ -39,51 +24,51 @@ export function normalizePublicUrl(raw) {
|
|
|
39
24
|
return null;
|
|
40
25
|
return `${url.origin}${url.pathname}`.replace(/\/+$/, "");
|
|
41
26
|
}
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
* `normalizePublicUrl`): a silently "fixed" entry would advertise a model the
|
|
45
|
-
* operator never picked. Notes are capped — they are one line of intent, and
|
|
46
|
-
* every session that asks for the menu pays for their tokens.
|
|
47
|
-
*/
|
|
27
|
+
/** Rejecting rather than repairing: a "fixed" entry would advertise a model the
|
|
28
|
+
* operator never picked. Notes are capped; every session pays for their tokens. */
|
|
48
29
|
export function normalizeModelMenu(raw) {
|
|
49
30
|
if (!Array.isArray(raw) || raw.length > 32)
|
|
50
31
|
return null;
|
|
51
32
|
const menu = [];
|
|
52
33
|
for (const item of raw) {
|
|
53
|
-
|
|
34
|
+
const ref = normalizeModelRef(item);
|
|
35
|
+
if (!ref)
|
|
54
36
|
return null;
|
|
55
|
-
const {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (thinking !== undefined && !isThinkingLevel(thinking))
|
|
37
|
+
const { thinking, note } = item;
|
|
38
|
+
// Repaired, not rejected: rows stored before the level was required have
|
|
39
|
+
// none, and dropping the menu over it would lose the pins.
|
|
40
|
+
const level = thinking === undefined ? "medium" : thinking;
|
|
41
|
+
if (!isThinkingLevel(level))
|
|
61
42
|
return null;
|
|
62
43
|
if (note !== undefined && typeof note !== "string")
|
|
63
44
|
return null;
|
|
64
45
|
const cleaned = note?.trim().slice(0, 200);
|
|
65
46
|
menu.push({
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
...(thinking !== undefined ? { thinking } : {}),
|
|
47
|
+
...ref,
|
|
48
|
+
thinking: level,
|
|
69
49
|
...(cleaned ? { note: cleaned } : {}),
|
|
70
50
|
});
|
|
71
51
|
}
|
|
72
52
|
return menu;
|
|
73
53
|
}
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
54
|
+
/** Existence is not checked: the catalog is the agent's, and a model that went
|
|
55
|
+
* away is reported by the call that fails. */
|
|
56
|
+
export function normalizeModelRef(raw) {
|
|
57
|
+
if (typeof raw !== "object" || raw === null)
|
|
58
|
+
return null;
|
|
59
|
+
const { provider, id } = raw;
|
|
60
|
+
if (typeof provider !== "string" || !provider.trim())
|
|
61
|
+
return null;
|
|
62
|
+
if (typeof id !== "string" || !id.trim())
|
|
63
|
+
return null;
|
|
64
|
+
return { provider: provider.trim(), id: id.trim() };
|
|
65
|
+
}
|
|
66
|
+
/** Shape only: the catalog is code behind the Pi SDK, and an unknown name is
|
|
67
|
+
* ignored there, so a downgrade cannot lose a setting it cannot explain. */
|
|
81
68
|
export function normalizeExtensions(raw) {
|
|
82
69
|
return normalizeNames(raw);
|
|
83
70
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Shape only again: tools.ts owns the catalog, and a name it does not know is
|
|
86
|
-
* ignored there rather than rejected here, so a downgrade cannot lose one. */
|
|
71
|
+
/** Shape only, for the same reason: tools.ts owns the catalog. */
|
|
87
72
|
export function normalizeTools(raw) {
|
|
88
73
|
return normalizeNames(raw);
|
|
89
74
|
}
|
|
@@ -107,23 +92,19 @@ export class SettingsStore {
|
|
|
107
92
|
this.#db = db;
|
|
108
93
|
}
|
|
109
94
|
get() {
|
|
95
|
+
const titleModel = this.#json("titleModel", normalizeModelRef, "a {provider, id}");
|
|
110
96
|
return {
|
|
111
97
|
publicUrl: this.#value("publicUrl") ?? "",
|
|
112
98
|
modelMenu: this.#json("modelMenu", normalizeModelMenu, "a valid menu") ?? [],
|
|
99
|
+
...(titleModel ? { titleModel } : {}),
|
|
113
100
|
autoUpdate: this.#value("autoUpdate") === "1",
|
|
114
101
|
extensions: this.#json("extensions", normalizeExtensions, "a list of names") ?? [],
|
|
115
102
|
tools: this.#json("tools", normalizeTools, "a list of names") ?? [],
|
|
116
|
-
// `"drop"`: a
|
|
117
|
-
|
|
118
|
-
// every other tool declared beside it (see normalizeCustomTools).
|
|
119
|
-
customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw, [], "drop"), "a list of {name, spec}") ?? [],
|
|
103
|
+
// `"drop"`: a row the bundled catalog has since taken is redundant, not malformed.
|
|
104
|
+
customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw, [], "drop"), "a list of {name, toml}") ?? [],
|
|
120
105
|
};
|
|
121
106
|
}
|
|
122
|
-
/**
|
|
123
|
-
* A JSON-valued row, validated on the way out. Only a hand-edited row can be
|
|
124
|
-
* malformed, and it is named rather than silently served as the empty value:
|
|
125
|
-
* a setting that stopped applying without saying so is the bug this logs.
|
|
126
|
-
*/
|
|
107
|
+
/** A malformed row is named, not silently served as the empty value (§5). */
|
|
127
108
|
#json(key, normalize, expected) {
|
|
128
109
|
const raw = this.#value(key);
|
|
129
110
|
if (!raw)
|
|
@@ -141,42 +122,38 @@ export class SettingsStore {
|
|
|
141
122
|
log.warn(`settings.${key} is not ${expected} — ignoring it`);
|
|
142
123
|
return value;
|
|
143
124
|
}
|
|
144
|
-
/**
|
|
145
|
-
* that received it, so this never has to guess what the caller meant. */
|
|
125
|
+
/** Setters take already-normalized values: validation belongs at the boundary. */
|
|
146
126
|
setPublicUrl(publicUrl) {
|
|
147
127
|
this.#set("publicUrl", publicUrl);
|
|
148
128
|
return this.get();
|
|
149
129
|
}
|
|
150
|
-
/** Same contract: hand this `normalizeModelMenu`'s output, not raw input. */
|
|
151
130
|
setModelMenu(menu) {
|
|
152
131
|
this.#set("modelMenu", JSON.stringify(menu));
|
|
153
132
|
return this.get();
|
|
154
133
|
}
|
|
134
|
+
/** null switches auto-titling off. */
|
|
135
|
+
setTitleModel(ref) {
|
|
136
|
+
this.#set("titleModel", ref ? JSON.stringify(ref) : "");
|
|
137
|
+
return this.get();
|
|
138
|
+
}
|
|
155
139
|
setAutoUpdate(on) {
|
|
156
140
|
this.#set("autoUpdate", on ? "1" : "0");
|
|
157
141
|
return this.get();
|
|
158
142
|
}
|
|
159
|
-
/** Same contract again: hand this `normalizeExtensions`'s output. */
|
|
160
143
|
setExtensions(names) {
|
|
161
144
|
this.#set("extensions", JSON.stringify(names));
|
|
162
145
|
return this.get();
|
|
163
146
|
}
|
|
164
|
-
/** Same contract again: hand this `normalizeTools`'s output. */
|
|
165
147
|
setTools(names) {
|
|
166
148
|
this.#set("tools", JSON.stringify(names));
|
|
167
149
|
return this.get();
|
|
168
150
|
}
|
|
169
|
-
/** Same contract again: hand this `normalizeCustomTools`'s output. */
|
|
170
151
|
setCustomTools(tools) {
|
|
171
152
|
this.#set("customTools", JSON.stringify(tools));
|
|
172
153
|
return this.get();
|
|
173
154
|
}
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
* switch that turns it on must not be able to store one without the other:
|
|
177
|
-
* half of that pair is a switch nobody can explain — on and undeclared, or
|
|
178
|
-
* declared and invisible.
|
|
179
|
-
*/
|
|
155
|
+
/** A request that declares a tool and switches it on must not store one
|
|
156
|
+
* without the other. */
|
|
180
157
|
transact(work) {
|
|
181
158
|
return transact(this.#db, work);
|
|
182
159
|
}
|