chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes
Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool): - server/hub/ holds the hub-only code (devices, remote sessions, upload ingest, LAN gateway), required only in server mode; hub-boundary.test.js keeps local code from importing it. - A local install enrolls with `agenthub remote login` or Settings → AI tools → Company hub; the "Company account" AI Proxy (reserved name agenthub.company) sends a session through the hub's gateway, which adds a pool account's token — no credential reaches the machine. Only those sessions are uploaded, hourly and at their end; secret-shaped strings are redacted on the client. - Gateway refuses: unknown device (401), a message without a session id, an unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30. - New alert rule type remote_unmonitored: a company-account session its device never registered; a late registration clears the flag instead. - The hub imports a session once per upload round (chunks carry `more`), not once per chunk. Artifacts: - Share an artifact with chosen people (artifact_shares, PATCH sharedWith, GET /api/artifacts/people) besides everyone; a "Shared with you" tab. Only the owner or an admin sees or edits who it is shared with; audited. - Folders: each account's own arrangement (agenthub.artifacts.folders pref). - A claude.ai page keeps a copy viewable without the publishing account: its HTML file (and supporting files) snapshotted at publish, or the copy Claude saves for a Doc / Slides / Design in artifacts-export/ (a synchronous PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch), attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF). Fixes: - tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's tmux (stop the last pane, start again within 10 s failed with ENOENT). - shelves: a shelf restores beside changes in other files. - chat: a finished reply ends the turn even with no hook at all. - Settings → AI tools: profile list display tweaks.
This commit is contained in:
@@ -986,6 +986,18 @@ for (const [col, ddl] of [
|
||||
}
|
||||
}
|
||||
|
||||
// An artifact shared with chosen people (server mode): one row per person.
|
||||
// `artifacts.shared = 1` stays "everyone" (the Hub); these rows are on top.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS artifact_shares (
|
||||
artifact_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
shared_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
PRIMARY KEY (artifact_id, user_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_artifact_shares_user ON artifact_shares(user_id);
|
||||
`);
|
||||
|
||||
// One row per install of an artifact (server/lib/artifacts.js `install`):
|
||||
// who, when, and the folders it was written to — `artifacts.installs` only
|
||||
// counts. Installs before this table existed stay counted, not listed.
|
||||
@@ -2006,6 +2018,76 @@ db.exec(`
|
||||
PRIMARY KEY (user_id, tool)
|
||||
);
|
||||
`);
|
||||
// Remote clients (docs/plans/remote-clients/README.md). On the HUB (server
|
||||
// mode): `devices` — a user's enrolled machines, each with one device token
|
||||
// stored as its SHA-256 only (server/hub/devices.js); `remote_sessions` — the
|
||||
// company-account sessions a device registered, the pool account the gateway
|
||||
// keeps each one on, and what the gateway counted (server/hub/remote-sessions.js);
|
||||
// `remote_files` — how far each uploaded transcript file has been taken
|
||||
// (server/hub/remote-ingest.js). On a LOCAL install: `remote_uploads` — how far
|
||||
// each local transcript file has been sent (server/lib/remote-client.js).
|
||||
// Created on every install, empty and unread until used. Additive.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
last_seen_at TEXT,
|
||||
last_ip TEXT,
|
||||
revoked_at TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_user ON devices(user_id);
|
||||
CREATE TABLE IF NOT EXISTS remote_sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL,
|
||||
cwd TEXT NOT NULL DEFAULT '',
|
||||
tool TEXT NOT NULL DEFAULT 'claude',
|
||||
account TEXT,
|
||||
registered_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
ended_at TEXT,
|
||||
last_request_at TEXT,
|
||||
requests INTEGER NOT NULL DEFAULT 0,
|
||||
gw_input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
gw_output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
gw_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
gw_cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
last_upload_at TEXT,
|
||||
purged_at TEXT,
|
||||
-- 1 = never registered by its device: the gateway saw it (remote_gate=warn)
|
||||
unmonitored INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_remote_sessions_user ON remote_sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_remote_sessions_device ON remote_sessions(device_id);
|
||||
CREATE TABLE IF NOT EXISTS remote_files (
|
||||
session_id TEXT NOT NULL,
|
||||
rel_path TEXT NOT NULL,
|
||||
consumed INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
PRIMARY KEY (session_id, rel_path)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS remote_uploads (
|
||||
session_id TEXT NOT NULL,
|
||||
rel_path TEXT NOT NULL,
|
||||
sent INTEGER NOT NULL DEFAULT 0,
|
||||
sent_at TEXT,
|
||||
PRIMARY KEY (session_id, rel_path)
|
||||
);
|
||||
`);
|
||||
// Migrate: add `owner_id` to sessions — the user a session belongs to when no
|
||||
// workspace on this machine owns it: a remote client's company-account session
|
||||
// is owned by the device's user (server/hub/remote-ingest.js). Read by
|
||||
// scope.js before the account fallback. NULL everywhere else, so every
|
||||
// existing row reads exactly as before.
|
||||
try {
|
||||
db.prepare("SELECT owner_id FROM sessions LIMIT 1").get();
|
||||
} catch {
|
||||
db.prepare("ALTER TABLE sessions ADD COLUMN owner_id INTEGER").run();
|
||||
}
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_owner ON sessions(owner_id)`);
|
||||
// A user's OWN choice of one pool account per tool (Settings → AI tools →
|
||||
// Pool account, account-pins.js `setChoice`): below an admin's pin, above the
|
||||
// balancer, and only among the accounts the user's group may use.
|
||||
|
||||
Reference in New Issue
Block a user