discord: unit can run from a pinned copy; check --offline; status reads the effective tree (row 114, #1608)

Candidate manifest 68c60a79914692f3f6d4923f7bcff41de109ccca33bf6fba687fb88efa24bcac
from Rocko's round 1 packet (a94a943c9), on BASE 72c9ac36c. No
manifest path changed between BASE and the landing target. Filbert
approved round 1 in 28497 with no findings; the TOOLS.md paragraph
break is a doc-only follow-up (decision 137). Sage reran the full gate
at the landing target with the candidate applied and Docker live.
Nothing is installed or restarted; the live switch is Mos's step after
the SetSpark freeze lifts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-14 15:43:46 -05:00
co-authored by Claude Opus 5.5
parent a94a943c92
commit a1ddeabdfc
7 changed files with 207 additions and 13 deletions
+23 -1
View File
@@ -17,7 +17,7 @@ CHAT-03 conversation controller exists.
## Commands
```
scripts/discord.sh check <binding>
scripts/discord.sh check <binding> [--offline]
scripts/discord.sh run <binding> [--supervised]
scripts/discord.sh stop <binding>
scripts/discord.sh unlock <binding>
@@ -34,6 +34,9 @@ scripts/discord-service.sh render | install | uninstall | status <binding>
every listed channel over REST; opens one gateway connection, waits for
READY, closes it. Nothing is sent to a channel. Close code 4014 is reported
as the message-content intent not being granted in the developer portal.
`check --offline` stops after local binding/context/tool roots/pi/token-mode
validation and journal/STOP reporting. It makes no REST or gateway call
and says so in its last line. `run --offline` is a usage error.
- `run` refuses when `STOP` exists or an unresolved delivery cannot be
reconciled. Otherwise it starts pi, connects, and serves turns until
SIGTERM, SIGINT or `stop`. Run it under the service unit below, or by hand
@@ -121,6 +124,25 @@ at runtime, as before. Surviving logout and reboot needs
`uninstall` refuses while an instance is active. The log goes to journald:
`run` writes no message text to stderr, only ids, counts and state.
Pinned install: `scripts/discord-service.sh install --copy ID [--copies DIR]`
uses a full 40-character copy id under the shared mosaic-stack/copies
location. It requires the copy's Discord CLI and node_modules/.bin/pi,
runs Node directly on that CLI with --repo and WorkingDirectory set to
the copy, and retains the unit's restart, brake and stop behavior. Without
--copy the unit still runs the checkout; install explicitly recommends
pinning. Status reads effective WorkingDirectory, including drop-ins, and
prints the tree and copy id or checkout path.
Every live step requires Mos's go after the SetSpark freeze lifts. Re-pin:
make the new pushed copy with stack-copy.sh; run
`node <copy>/packages/discord/src/cli.mjs check <binding> --repo <copy> --offline`;
preserve the old installed unit file; install --copy ID; reload/restart on
Mos's go. The copy has no scripts/discord.sh, so use its CLI directly.
Binding context.files resolve against the copy: a file not copied refuses
at start/check. Rollback restores the saved unit file, daemon-reloads and
restarts only on Mos's go; retain both copies. This connector pin is
independent of the bus/Console/CLI pin and stack-copy.sh check.
## The binding
Deployment policy for one seat on one server. It carries Discord ids of real
+11 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Usage:
// mosaic-discord check <binding> [--config PATH] [--repo PATH]
// mosaic-discord check <binding> [--config PATH] [--repo PATH] [--offline]
// mosaic-discord run <binding> [--config PATH] [--repo PATH] [--supervised]
// mosaic-discord stop <binding> [--config PATH]
// mosaic-discord unlock <binding> [--config PATH]
@@ -61,7 +61,7 @@ import { createConnector } from "./connector.mjs";
import { ensureJournal, requestStop, stopRequested, readPid, stopTarget, writePid, clearPid, unlock, recover, appendReload, BRAKE_EXIT } from "./journal.mjs";
const USAGE = [
"usage: mosaic-discord check <binding> [--config PATH] [--repo PATH]",
"usage: mosaic-discord check <binding> [--config PATH] [--repo PATH] [--offline]",
" mosaic-discord run <binding> [--config PATH] [--repo PATH] [--supervised]",
" mosaic-discord stop <binding> [--config PATH]",
" mosaic-discord unlock <binding> [--config PATH]",
@@ -70,7 +70,7 @@ const USAGE = [
].join("\n");
function parse(argv) {
const opts = { command: null, binding: null, config: defaultConfigPath(), repo: process.cwd(), supervised: false };
const opts = { command: null, binding: null, config: defaultConfigPath(), repo: process.cwd(), supervised: false, offline: false };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--config" || a === "--repo") {
@@ -80,6 +80,8 @@ function parse(argv) {
opts.command = "help";
} else if (a === "--supervised") {
opts.supervised = true;
} else if (a === "--offline") {
opts.offline = true;
} else if (a.startsWith("--")) throw new DiscordError(`unknown argument: ${a}\n${USAGE}`, 4);
else if (opts.command === null) opts.command = a;
else if (opts.binding === null) opts.binding = a;
@@ -89,6 +91,7 @@ function parse(argv) {
if (!["check", "run", "stop", "unlock", "recover", "reload"].includes(opts.command)) throw new DiscordError(USAGE, 4);
if (opts.binding === null) throw new DiscordError(`${opts.command} needs a binding name\n${USAGE}`, 4);
if (opts.supervised && opts.command !== "run") throw new DiscordError(`--supervised applies to run only\n${USAGE}`, 4);
if (opts.offline && opts.command !== "check") throw new DiscordError(`--offline applies to check only\n${USAGE}`, 4);
return opts;
}
@@ -123,6 +126,11 @@ async function check(opts) {
say(toolRoots ? `tools: ${enabledToolNames(toolRoots).join(",")}; roots ${toolRoots.roots.map((r) => `${r.name}=${r.path}${r.write ? " (writable)" : ""}`).join(" ")}, ${toolRoots.maxCallsPerTurn} calls/message, ${toolRoots.maxFileBytes} bytes/file${toolRoots.web ? `, web via ${toolRoots.web.searxng} (${toolRoots.web.maxFetchBytes} bytes/page)` : ""}${toolRoots.setspark ? `, setspark via ${toolRoots.setspark.baseUrl} as ${toolRoots.setspark.principal}` : ""}` : "tools: none");
say(`token file mode 0600 ok; STOP ${stopRequested(journalDir) ? "PRESENT" : "absent"}`);
if (opts.offline) {
say("offline check passed; no REST or gateway calls were made");
return;
}
const rest = createRest({ token, log: warn });
const me = await rest.getMe();
if (me.id !== binding.botUserId) throw new DiscordError(`token belongs to bot ${me.id}, binding says ${binding.botUserId}`);
@@ -0,0 +1,26 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {spawnSync} from 'node:child_process';
import {readFileSync,writeFileSync,mkdirSync,unlinkSync,chmodSync,existsSync} from 'node:fs';
import {join,dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import {makeRoot} from './helpers.mjs';
const ROOT=fileURLToPath(new URL('../../../',import.meta.url)).replace(/\/$/,'');
const SCRIPT=join(ROOT,'scripts/discord-service.sh');
function setup(){const root=makeRoot(),copies=join(root,'copies'),id='a'.repeat(40),copy=join(copies,id),units=join(root,'units');mkdirSync(join(copy,'packages/discord/src'),{recursive:true});mkdirSync(join(copy,'node_modules/.bin'),{recursive:true});writeFileSync(join(copy,'packages/discord/src/cli.mjs'),'// fixture');writeFileSync(join(copy,'node_modules/.bin/pi'),'#!/bin/sh\nexit 0\n',{mode:0o755});const env={PATH:process.env.PATH,HOME:root,XDG_CONFIG_HOME:join(root,'config')};const run=args=>spawnSync(SCRIPT,args,{env,encoding:'utf8',timeout:5000});return {root,copies,id,copy,units,run,env};}
test('pinned Discord render/install and missing prerequisites; checkout bytes unchanged',()=>{
const s=setup(),args=['--copy',s.id,'--copies',s.copies],r=s.run(['render',...args]);assert.equal(r.status,0,r.stderr);
assert.ok(r.stdout.includes(`WorkingDirectory=${s.copy}\n`));assert.ok(r.stdout.includes(`ExecStart=${process.execPath} ${s.copy}/packages/discord/src/cli.mjs run %i --supervised --repo ${s.copy}\n`));
for(const line of r.stdout.split('\n').filter(l=>/^(ExecStart|WorkingDirectory)=/.test(l)))assert.ok(!line.includes(ROOT),line);
const legacy=s.run(['render']);assert.equal(legacy.status,0,legacy.stderr);
const nodeDir=dirname(process.execPath),originalPath='/usr/local/bin:/usr/bin:/bin',path=originalPath.split(':').includes(nodeDir)?originalPath:nodeDir+':'+originalPath;
const expected=readFileSync(join(ROOT,'packages/discord/systemd/[email protected]'),'utf8').replaceAll('@REPO@',ROOT).replaceAll('@PATH@',path);assert.equal(legacy.stdout,expected);
assert.equal(r.stdout.split('\n').filter(l=>!/^(ExecStart|WorkingDirectory)=/.test(l)).join('\n'),legacy.stdout.split('\n').filter(l=>!/^(ExecStart|WorkingDirectory)=/.test(l)).join('\n'));
const installed=s.run(['install',...args,'--dir',s.units,'--no-reload']);assert.equal(installed.status,0,installed.stderr);assert.equal(readFileSync(join(s.units,'[email protected]'),'utf8'),r.stdout);
for(const id of ['','short','b'.repeat(40)])assert.equal(s.run(['install','--copy',id,'--copies',s.copies,'--dir',join(s.root,'missing'),'--no-reload']).status,1);
assert.ok(!existsSync(join(s.root,'missing/[email protected]')));
const cli=join(s.copy,'packages/discord/src/cli.mjs');unlinkSync(cli);assert.equal(s.run(['install',...args,'--dir',join(s.root,'missing-cli'),'--no-reload']).status,1);assert.ok(!existsSync(join(s.root,'missing-cli/[email protected]')));writeFileSync(cli,'// fixture');unlinkSync(join(s.copy,'node_modules/.bin/pi'));assert.equal(s.run(['install',...args,'--dir',join(s.root,'missing-pi'),'--no-reload']).status,1);assert.ok(!existsSync(join(s.root,'missing-pi/[email protected]')));
});
test('Discord status reports effective WorkingDirectory from a fake user manager',()=>{
const s=setup(),bin=join(s.root,'bin');mkdirSync(bin);const script=join(bin,'systemctl');writeFileSync(script,`#!/bin/sh\ncase "$*" in *show*)echo 'WorkingDirectory=${s.copy}';; *)echo inactive;;esac\n`);chmodSync(script,0o755);s.env.PATH=bin+':'+s.env.PATH;const r=s.run(['status','fixture']);assert.equal(r.status,0,r.stderr);assert.ok(r.stdout.includes(`copy: ${s.id}`));assert.ok(r.stdout.includes(`tree: ${s.copy}`));
});
@@ -0,0 +1,26 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {spawnSync} from 'node:child_process';
import {writeFileSync,chmodSync,unlinkSync,existsSync} from 'node:fs';
import {join} from 'node:path';
import {fileURLToPath} from 'node:url';
import {makeRoot,makeRepo,makeDeployment} from './helpers.mjs';
const CLI=fileURLToPath(new URL('../src/cli.mjs',import.meta.url));
function setup(){
const root=makeRoot(),repo=makeRepo(root),d=makeDeployment(root,{}, {tokenContent:'MTAw.abcdefghijklmnopqrstuvwxyz0123456789'}),calls=join(root,'network-called'),preload=join(root,'no-network.mjs');
writeFileSync(preload,`import {writeFileSync} from 'node:fs';const refuse=()=>{writeFileSync(${JSON.stringify(calls)},'called');throw Error('fixture refuses network')};globalThis.fetch=refuse;globalThis.WebSocket=class{constructor(){refuse()}};`);
const run=(command='check',args=[])=>spawnSync(process.execPath,['--import',preload,CLI,command,d.raw.name,'--config',d.config,'--repo',repo,...args],{encoding:'utf8',timeout:5000});
return {root,repo,d,calls,run};
}
test('check --offline validates local fixture without REST or gateway calls',()=>{
const s=setup(),r=s.run('check',['--offline']);assert.equal(r.status,0,r.stderr);assert.match(r.stdout,/token file mode 0600 ok; STOP absent/);assert.match(r.stdout,/offline check passed; no REST or gateway calls were made\n$/);assert.ok(!existsSync(s.calls));
// Online check still reaches REST: the trap fails it immediately.
const online=s.run();assert.equal(online.status,1);assert.ok(existsSync(s.calls));assert.match(online.stderr,/fixture refuses network/);
});
test('check --offline refuses token mode and missing context under the selected repo',()=>{
const s=setup();chmodSync(s.d.tokenFile,0o644);const bad=s.run('check',['--offline']);assert.equal(bad.status,2,bad.stderr);assert.match(bad.stderr,/0600/);assert.ok(!existsSync(s.calls));
chmodSync(s.d.tokenFile,0o600);unlinkSync(join(s.repo,'contracts/CONSTITUTION.md'));const context=s.run('check',['--offline']);assert.equal(context.status,2,context.stderr);assert.ok(!existsSync(s.calls));
});
test('run --offline is a usage refusal before reading config or networking',()=>{
const s=setup();unlinkSync(s.d.config);const r=s.run('run',['--offline']);assert.equal(r.status,4,r.stderr);assert.match(r.stderr,/--offline/);assert.ok(!existsSync(s.calls));
});