shadok-ai 0.17.12 → 0.17.14
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 +1 -0
- package/context/ledger-skill/ledger.mjs +50 -12
- package/dist/server.js +61 -8
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -859,6 +859,7 @@ machine, which silently shifts every daily prompt on a server running in UTC.
|
|
|
859
859
|
| `SHADOK_PILOT_PROMPT=0` | don't inject the cockpit system prompt |
|
|
860
860
|
| `SHADOK_GREETING=0` | the lead agent starts silent — no first-message greeting |
|
|
861
861
|
| `SHADOK_LEDGER=0` | turn OFF the shared-ledger reflex — a per-instance state table siblings read/write so they stop re-surfacing resolved work, plus a `⟦ledger⟧` delta pushed ahead of each message (**ON by default**; `SHADOK_LEDGER=0` or the GUI/config setting turns it off) |
|
|
862
|
+
| `SHADOK_SEED_SKILLS=0` | don't rewrite the machine-wide skill scripts at boot — for a build run side by side with another instance, so it doesn't hand that instance's agents scripts newer than the server they call |
|
|
862
863
|
| `SHADOK_RESUME_SUMMARY=1` | don't auto-answer the resume-from-summary prompt |
|
|
863
864
|
| `SHADOK_SSH_IDENTITY=0` · `SHADOK_FORCE_SSH_IDENTITY=1` | disable / force the Docker SSH identity |
|
|
864
865
|
| `TELEGRAM_BOT_TOKEN` · `TELEGRAM_ALLOWED_CHATS` | override the stored config |
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
// with no port/key), it reads and writes the local file directly —
|
|
15
15
|
// SHADOK_LEDGER_FILE if the server handed one, else the legacy global file.
|
|
16
16
|
// Nothing about the single-group / off-server case changes.
|
|
17
|
+
//
|
|
18
|
+
// The SAME fallback catches a server that is reachable but OLDER than this
|
|
19
|
+
// script: skills are seeded machine-wide at every boot, so any build booted on
|
|
20
|
+
// the host (a side-by-side verification run) rewrites them under the running
|
|
21
|
+
// server, and a route this file calls may simply not exist there. A 404 is that
|
|
22
|
+
// fact, and it must degrade to the local table instead of failing the write —
|
|
23
|
+
// see invariant 57. Only a 404: a 401/403 is a real auth problem with a human
|
|
24
|
+
// action behind it (invariant 33) and stays loud.
|
|
17
25
|
import fs from "node:fs";
|
|
18
26
|
import os from "node:os";
|
|
19
27
|
import path from "node:path";
|
|
@@ -61,8 +69,10 @@ function mintId(rows) {
|
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
/** Call the server. Exits with a clear message on a transport/HTTP failure —
|
|
64
|
-
* the same shape as the secrets skill.
|
|
65
|
-
|
|
72
|
+
* the same shape as the secrets skill. With `soft404`, a 404 returns null
|
|
73
|
+
* instead of exiting: the caller then has a local-file path to degrade to, and
|
|
74
|
+
* a 404 means THIS ROUTE does not exist on the running build (invariant 57). */
|
|
75
|
+
async function api(method, p, body, { soft404 = false } = {}) {
|
|
66
76
|
let res;
|
|
67
77
|
try {
|
|
68
78
|
res = await fetch(BASE + p, {
|
|
@@ -82,6 +92,14 @@ async function api(method, p, body) {
|
|
|
82
92
|
process.exit(1);
|
|
83
93
|
}
|
|
84
94
|
const raw = await res.text();
|
|
95
|
+
if (res.status === 404 && soft404) {
|
|
96
|
+
console.error(
|
|
97
|
+
`ledger: the running cockpit does not serve ${method} ${p} (it predates this skill copy) — ` +
|
|
98
|
+
`using the local table ${FILE}. Skills are re-seeded at every boot, so a newer build ` +
|
|
99
|
+
`booted on this host can leave them ahead of the running server.`,
|
|
100
|
+
);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
85
103
|
if (!res.ok) {
|
|
86
104
|
let msg = raw.slice(0, 300);
|
|
87
105
|
try {
|
|
@@ -131,7 +149,10 @@ function fmt(e, now) {
|
|
|
131
149
|
/** The rows a read works over: on-server, the server-scoped table (own group ∪
|
|
132
150
|
* public); off-server, the local file. */
|
|
133
151
|
async function visibleRows() {
|
|
134
|
-
if (useServer)
|
|
152
|
+
if (useServer) {
|
|
153
|
+
const r = await api("GET", "/ledger", undefined, { soft404: true });
|
|
154
|
+
if (r) return r.entries ?? [];
|
|
155
|
+
}
|
|
135
156
|
return loadFile(FILE);
|
|
136
157
|
}
|
|
137
158
|
|
|
@@ -199,13 +220,9 @@ async function doRecord(rest) {
|
|
|
199
220
|
public: f.public === true ? true : undefined,
|
|
200
221
|
};
|
|
201
222
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// upsert to it, so a cross-group clobber is impossible by construction.
|
|
206
|
-
row = (await api("POST", "/ledger/record", patch)).row;
|
|
207
|
-
} else {
|
|
208
|
-
// Off-server fallback: write the local file directly (mono-tenant / hand-run).
|
|
223
|
+
// Off-server (or older-server) fallback: write the local file directly
|
|
224
|
+
// (mono-tenant / hand-run / a build that does not serve the route).
|
|
225
|
+
const recordLocally = () => {
|
|
209
226
|
const rows0 = loadFile(FILE);
|
|
210
227
|
let next;
|
|
211
228
|
try {
|
|
@@ -219,8 +236,29 @@ async function doRecord(rest) {
|
|
|
219
236
|
console.error(String(e?.message ?? e));
|
|
220
237
|
process.exit(2);
|
|
221
238
|
}
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
try {
|
|
240
|
+
saveFile(FILE, next);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// Worth naming rather than a stack: on a multi-tenant instance the table
|
|
243
|
+
// is 0600 in the SERVER's home, so a dropped agent cannot write it — the
|
|
244
|
+
// row is lost and a human has to update/restart the instance.
|
|
245
|
+
console.error(
|
|
246
|
+
`ledger: could not write ${FILE} — ${e?.message ?? e}. The record was NOT saved; ` +
|
|
247
|
+
`tell the operator (the cockpit is older than this skill copy, see invariant 57).`,
|
|
248
|
+
);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
return byId ? resolveId(next, f.id) : next.find((r) => normEntity(r.entity) === normEntity(f.entity));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
let row;
|
|
255
|
+
if (useServer) {
|
|
256
|
+
// The server stamps the authoring group (from the session) and scopes the
|
|
257
|
+
// upsert to it, so a cross-group clobber is impossible by construction.
|
|
258
|
+
const r = await api("POST", "/ledger/record", patch, { soft404: true });
|
|
259
|
+
row = r ? r.row : recordLocally();
|
|
260
|
+
} else {
|
|
261
|
+
row = recordLocally();
|
|
224
262
|
}
|
|
225
263
|
|
|
226
264
|
const k = rowKind(row);
|
package/dist/server.js
CHANGED
|
@@ -1728,6 +1728,27 @@ function mtTokenGate(req, res) {
|
|
|
1728
1728
|
}
|
|
1729
1729
|
return true;
|
|
1730
1730
|
}
|
|
1731
|
+
/**
|
|
1732
|
+
* The READ counterpart of `mtTokenGate`: multi-tenant + admin, but WITHOUT the
|
|
1733
|
+
* `requestFromBrowser` (Origin) check. A browser omits `Origin` on a same-origin
|
|
1734
|
+
* GET, so gating a READ on it 403s the cockpit's own status fetch — which is
|
|
1735
|
+
* exactly what hid the per-user sign-in list (the GET returned 403, so it never
|
|
1736
|
+
* rendered, while the POST writes that DO send Origin worked). The Origin check
|
|
1737
|
+
* is CSRF protection for WRITES (invariant 26); a read of login status (labels +
|
|
1738
|
+
* loggedIn booleans, never a token) has no such surface, so it must not require
|
|
1739
|
+
* one.
|
|
1740
|
+
*/
|
|
1741
|
+
function mtReadGate(req, res) {
|
|
1742
|
+
if (DEPLOYMENT_MODE !== "multi-tenant") {
|
|
1743
|
+
res.status(400).json({ error: "not applicable: this instance is single-tenant" });
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
if (currentAccount(req)?.role !== "admin") {
|
|
1747
|
+
res.status(403).json({ error: "admins only" });
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1731
1752
|
app.post("/mt-token/start", async (req, res) => {
|
|
1732
1753
|
if (!mtTokenGate(req, res))
|
|
1733
1754
|
return;
|
|
@@ -1780,7 +1801,10 @@ function loginUsers() {
|
|
|
1780
1801
|
return out;
|
|
1781
1802
|
}
|
|
1782
1803
|
app.get("/agent-login", (req, res) => {
|
|
1783
|
-
|
|
1804
|
+
// A READ: use the origin-less gate. A browser sends no `Origin` on a
|
|
1805
|
+
// same-origin GET, so `mtTokenGate` (which requires one) would 403 the
|
|
1806
|
+
// cockpit's own status fetch and the per-user sign-in list would never render.
|
|
1807
|
+
if (!mtReadGate(req, res))
|
|
1784
1808
|
return;
|
|
1785
1809
|
const mode = agentAuthForCwd(loadConfig(), process.cwd());
|
|
1786
1810
|
const users = loginUsers().map(({ user, home, label }) => {
|
|
@@ -7350,6 +7374,25 @@ try {
|
|
|
7350
7374
|
catch {
|
|
7351
7375
|
/* best effort — the CTA still starts an agent, just without the role */
|
|
7352
7376
|
}
|
|
7377
|
+
/**
|
|
7378
|
+
* Do we rewrite the machine-wide skill scripts at this boot?
|
|
7379
|
+
*
|
|
7380
|
+
* The skills below are seeded OUTSIDE the running tree — the server user's
|
|
7381
|
+
* `~/.claude/skills`, plus `/etc/claude-code/.claude/skills` in multi-tenant —
|
|
7382
|
+
* and they are rewritten at EVERY boot. That is what makes a merged SKILL.md
|
|
7383
|
+
* reach existing agents with no reload, and it is also a trap: a build started
|
|
7384
|
+
* side by side to verify a change (the sanctioned way, see CLAUDE.md "Running
|
|
7385
|
+
* YOUR build") rewrites the shared skills UNDER the running production server,
|
|
7386
|
+
* leaving every agent on the host with scripts newer than the server they call.
|
|
7387
|
+
* Measured: the ledger skill began posting a route the running build did not
|
|
7388
|
+
* serve, so `record` answered 404 host-wide while `check` kept working — a
|
|
7389
|
+
* half-broken tool, which is worse than a broken one (invariant 57).
|
|
7390
|
+
*
|
|
7391
|
+
* `SHADOK_SEED_SKILLS=0` is the fourth property of a safe side-by-side run,
|
|
7392
|
+
* next to `PORT`, `SHADOK_VERSION_CHECK_MIN=0` and the per-dir Telegram token.
|
|
7393
|
+
* Default ON: an ordinary boot must keep refreshing them.
|
|
7394
|
+
*/
|
|
7395
|
+
const SEED_SKILLS = (process.env.SHADOK_SEED_SKILLS ?? "").trim() !== "0";
|
|
7353
7396
|
// Install/refresh the bundled "shadok-scheduler" skill so agents can set up
|
|
7354
7397
|
// their own channel's crons in plain language. Server-owned, overwritten each
|
|
7355
7398
|
// boot to stay current.
|
|
@@ -7374,7 +7417,8 @@ function seedSchedulerSkill() {
|
|
|
7374
7417
|
/* best effort — scheduling still works via the GUI/Telegram/API */
|
|
7375
7418
|
}
|
|
7376
7419
|
}
|
|
7377
|
-
|
|
7420
|
+
if (SEED_SKILLS)
|
|
7421
|
+
seedSchedulerSkill();
|
|
7378
7422
|
// Install/refresh the bundled "shadok-secrets" skill, so an agent that obtains
|
|
7379
7423
|
// a credential can keep it for the next one. Server-owned, overwritten each
|
|
7380
7424
|
// boot to stay current — same contract as the scheduler skill above.
|
|
@@ -7396,7 +7440,8 @@ function seedSecretsSkill() {
|
|
|
7396
7440
|
/* best effort — the vault still works from the GUI and Telegram */
|
|
7397
7441
|
}
|
|
7398
7442
|
}
|
|
7399
|
-
|
|
7443
|
+
if (SEED_SKILLS)
|
|
7444
|
+
seedSecretsSkill();
|
|
7400
7445
|
// Install/refresh the bundled "shadok-reload" skill so an agent can respawn
|
|
7401
7446
|
// itself to pick up a changed pilot prompt or newly-seeded skills. Server-owned,
|
|
7402
7447
|
// overwritten each boot — same contract as the skills above.
|
|
@@ -7416,7 +7461,8 @@ function seedReloadSkill() {
|
|
|
7416
7461
|
/* best effort — no self-reload skill just means a manual restart, never a crash */
|
|
7417
7462
|
}
|
|
7418
7463
|
}
|
|
7419
|
-
|
|
7464
|
+
if (SEED_SKILLS)
|
|
7465
|
+
seedReloadSkill();
|
|
7420
7466
|
// Install/refresh the bundled "shadok-ledger" skill so agents can verify a
|
|
7421
7467
|
// status before asserting/acting, and record resolutions. Server-owned,
|
|
7422
7468
|
// overwritten each boot — same contract as the skills above. Seeded regardless
|
|
@@ -7438,7 +7484,8 @@ function seedLedgerSkill() {
|
|
|
7438
7484
|
/* best effort — an absent skill just means no ledger, never a crash */
|
|
7439
7485
|
}
|
|
7440
7486
|
}
|
|
7441
|
-
|
|
7487
|
+
if (SEED_SKILLS)
|
|
7488
|
+
seedLedgerSkill();
|
|
7442
7489
|
/**
|
|
7443
7490
|
* The `shadok-files` skill — handing a file to the person driving the cockpit.
|
|
7444
7491
|
*
|
|
@@ -7462,7 +7509,8 @@ function seedFilesSkill() {
|
|
|
7462
7509
|
/* best effort — a missing skill means paths get printed, not a broken boot */
|
|
7463
7510
|
}
|
|
7464
7511
|
}
|
|
7465
|
-
|
|
7512
|
+
if (SEED_SKILLS)
|
|
7513
|
+
seedFilesSkill();
|
|
7466
7514
|
// Install/refresh the bundled "shadok-ai-agents" skill so an agent in ANY repo
|
|
7467
7515
|
// (not just this one) can spawn/pilot other agents via pilotctl. It used to live
|
|
7468
7516
|
// only in the repo's .claude/skills — a project skill invisible to agents working
|
|
@@ -7484,13 +7532,18 @@ function seedAgentsSkill() {
|
|
|
7484
7532
|
/* best effort — no agents skill just means no cross-agent piloting, never a crash */
|
|
7485
7533
|
}
|
|
7486
7534
|
}
|
|
7487
|
-
|
|
7535
|
+
if (SEED_SKILLS)
|
|
7536
|
+
seedAgentsSkill();
|
|
7488
7537
|
// Multi-tenant: also install every skill machine-wide (Enterprise tier), so the
|
|
7489
7538
|
// per-tenant `sk-*` agents — which run under their own HOME, not the server's —
|
|
7490
7539
|
// get them without a per-HOME copy. Mono-tenant/dev keeps the personal install
|
|
7491
7540
|
// above and never touches /etc.
|
|
7492
|
-
if (DEPLOYMENT_MODE === "multi-tenant")
|
|
7541
|
+
if (DEPLOYMENT_MODE === "multi-tenant" && SEED_SKILLS)
|
|
7493
7542
|
seedSystemSkills();
|
|
7543
|
+
// Say it: a boot that did NOT refresh the shared skills must be visible, or a
|
|
7544
|
+
// side-by-side run and a broken seed look the same in the log.
|
|
7545
|
+
if (!SEED_SKILLS)
|
|
7546
|
+
console.log("skills: SHADOK_SEED_SKILLS=0 — left the machine-wide skills alone");
|
|
7494
7547
|
/** Run a package-manager install. Linux managers need root; if we aren't root,
|
|
7495
7548
|
* go through NON-interactive sudo so a password prompt fails fast rather than
|
|
7496
7549
|
* hanging the boot. brew (needsRoot:false) must never be sudo'd. */
|