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:
+39
-8
@@ -21,7 +21,8 @@
|
||||
* stash would shift), and nothing drops one except a clean restore or a
|
||||
* Delete with `confirm: true`. They are repo-wide, so repo root only
|
||||
* (ESUBDIR), never during a merge/cherry-pick/revert/rebase (EMERGING), and
|
||||
* a restore needs a clean tree (EDIRTY). A conflicted restore keeps its
|
||||
* a restore needs an empty index and none of its own files changed (EDIRTY;
|
||||
* changes elsewhere stay). A conflicted restore keeps its
|
||||
* shelf and leaves the unmerged files to `resolve`. A write that moves
|
||||
* files under an agent the route reports as working asks for
|
||||
* `confirm: true` first (EAGENTRUNNING). A merge is the
|
||||
@@ -617,11 +618,9 @@ async function runGitAction(dir, action, body = {}, opts = {}) {
|
||||
return { ...done(action, await dropShelf(dir, entry.sha)), sha: entry.sha };
|
||||
}
|
||||
const entry = await findShelf(dir, body.sha);
|
||||
// A restore never mixes with new work: on a clean tree its result is
|
||||
// exactly the shelf, and a conflict can only be between it and HEAD.
|
||||
if (await isDirty(dir)) throw bad("EDIRTY", "working tree has uncommitted changes");
|
||||
const before = await assertRestorable(dir, entry);
|
||||
assertAgentConfirmed(opts, body, "restoring a shelf");
|
||||
return applyShelf(dir, entry);
|
||||
return applyShelf(dir, entry, before);
|
||||
}
|
||||
|
||||
if (action === "ignore") return ignorePaths(dir, body);
|
||||
@@ -914,13 +913,44 @@ async function dropShelf(dir, sha) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a shelf back on a clean tree. `--index` first, so what was staged comes
|
||||
* Put a shelf back onto a tree `assertRestorable` let through. `--index` first, so what was staged comes
|
||||
* back staged; when only the index cannot be rebuilt git refuses before it
|
||||
* touches the tree, and the shelf is applied without it. A clean apply drops
|
||||
* the entry; a conflicted one KEEPS it and leaves the unmerged files to the
|
||||
* `resolve` flow (ours = the branch, theirs = the shelf).
|
||||
*/
|
||||
async function applyShelf(dir, entry) {
|
||||
/**
|
||||
* A restore never mixes with new work in the files it touches: nothing may be
|
||||
* staged (`stash apply --index` against a changed index stops halfway, its
|
||||
* untracked files already written) and no change may be in one of the shelf's
|
||||
* own files. Changes elsewhere stay where they are — a partial Shelve (n)
|
||||
* leaves the unticked files behind, and refusing them made such a shelf
|
||||
* impossible to restore. So a conflict is still only between the shelf and
|
||||
* HEAD. Answers the tree's status before the restore, for `applyShelf`.
|
||||
*/
|
||||
async function assertRestorable(dir, entry) {
|
||||
const status = await laneStatus(dir);
|
||||
if (status.some((e) => e.code[0] !== " " && e.code[0] !== "?"))
|
||||
throw bad("EDIRTY", "the index has staged changes: commit or unstage them first");
|
||||
const mine = new Set(entry.files.map((f) => f.path));
|
||||
const overlap = status
|
||||
.flatMap((e) => [e.path, e.from].filter(Boolean))
|
||||
.filter((p) => mine.has(p));
|
||||
if (overlap.length)
|
||||
throw Object.assign(
|
||||
bad("EDIRTY", `files in this shelf have changed since: ${overlap.join(", ")}`),
|
||||
{ paths: [...new Set(overlap)] }
|
||||
);
|
||||
return status;
|
||||
}
|
||||
|
||||
const statusKey = (status) =>
|
||||
status
|
||||
.map((e) => `${e.code}\0${e.path}\0${e.from || ""}`)
|
||||
.sort()
|
||||
.join("\n");
|
||||
|
||||
async function applyShelf(dir, entry, before = []) {
|
||||
const kept = (e, conflicts) => ({
|
||||
...done("unshelve", e.git || { stdout: "", stderr: "" }),
|
||||
sha: entry.sha,
|
||||
@@ -933,7 +963,8 @@ async function applyShelf(dir, entry) {
|
||||
} catch (e) {
|
||||
const conflicts = await unmergedPaths(dir);
|
||||
if (conflicts.length) return kept(e, conflicts);
|
||||
if (await isDirty(dir)) throw e;
|
||||
// Only when git refused before touching the tree is a retry safe.
|
||||
if (statusKey(await laneStatus(dir)) !== statusKey(before)) throw e;
|
||||
try {
|
||||
result = await git(dir, ["stash", "apply", entry.sha]);
|
||||
} catch (e2) {
|
||||
|
||||
Reference in New Issue
Block a user