Skip to content

feat: export a whole Agent from the dashboard, past the 25 MiB chat limit - #1136

Merged
AustinKelsay merged 10 commits into
mainfrom
feat/agent-export-20261029
Oct 30, 2026
Merged

AustinKelsay merged 10 commits into
mainfrom
feat/agent-export-20261029

Conversation

@AustinKelsay

@AustinKelsay AustinKelsay commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The prompt-driven migration flow delivers exports as chat attachments and stops at 24 MiB. This adds an authenticated Export page and background exporter, with a streaming, resumable download that has no attachment-size cap. Typing /export in web chat opens the page.

Agent → Export → survey → choose Brains/sites → create export
                 native Hermes grant → bundled finite-export plugin
Download → dashboard access check → operation-local Core grant → plugin file stream

The plugin packages the move-site exporter as finite-agent-export/1, adds --everything, and keeps media and large workspace files. The manifest lists file hashes, exclusions and incomplete items. Native Hermes and the existing Agent-access grant enforce authority; this adds no Core, Runner or schema changes.

Credential files and archives stay out. Memories and scheduled-job strings are redacted for known credential patterns. Other supported text and SQLite databases are checked; SQLite copies are rebuilt from live rows and their schema SQL is scanned. Chats, media and other binary files are exported unchanged, so this is not a credential-free archive. The page and archive README disclose that boundary.

One survey/export runs at a time. Admission checks and a running low-disk guard cover both phases; job-record failure kills the worker. Cancellation removes staging, completed downloads survive a Hermes restart, and completed exports expire after seven days. Exporting does not retire or delete the Agent.

Evidence

  • Before: the move-script attachment flow reports fits: false above 24 MiB.
    After: real retained dashboard + source-built Hermes/exporter on a synthetic Agent produced 1,373,919,185 bytes. Browser download and a transfer dropped at 512 MiB then resumed had the same hash as the on-Agent file. All 228 manifest hashes matched; none of 20 planted credentials appeared. This is local synthetic proof, not destination-import acceptance.
  • Final polish at 815a3c06: adds a regression for ? in SQLite filenames and aligns older-Runtime documentation with the actual unsupported-page message. The packaged suite passes 8/8. The added test catches a credential-leaking URI mutant that the previous suite missed. Ruff and diff checks pass. Product implementation is unchanged from d7fcc454.
  • Combined release verification: retained dashboard unit tests 447 passed, plus the opt-in real Hosted Device deletion proof; lint, typecheck and production build pass. Seven relevant browser files pass. Sealed Brain/Sites inventory 30/30, inference 10/10, stop 7/7, skills 8/8, and the Brain composition/alias proof 2/2 pass with the new package.
  • Lifecycle/resources: partial export, cancel, Hermes restart, killed-worker recovery, older Runtime and real low-disk tests pass. A 24 GiB APFS test volume refused a survey at 15 GiB free and stopped an export at 8 GiB. A local production-mode dashboard added about 8 MiB RSS during a throttled 1 GiB download.
  • Availability boundary: 731 synthetic writes to Hermes SessionDB completed during export/download, and all status probes answered. This proves store responsiveness, not a real Finite Chat/model round trip. Core grants, WorkOS and the Caddy edge were stand-ins in the export proof; live Runner/Kata and Chat canary remain release gates.
  • Review and CI: Alex passes final source 815a3c06 in his follow-up review. Fable independently approves source 815a3c06 and retained 34b30e29, including 8 packaged tests, 8 credential-leak mutation checks and 18 focused dashboard tests. All selected jobs in final CI run 41077983130 passed. Root dismissed the two superseded changes-requested reviews with reasons tied to the reproduced fixes and later pass reports; this is a maintainer reconciliation, not an approving review from Alex. Normal merge-queue checks remain. These are source and synthetic qualifications, not production verification.

Merge Danger

Door: two-way

Dashboard and Runtime may ship in either order. An older Runtime shows “This agent does not support this page yet” and exposes no export controls. Reverting removes the feature, but existing exports remain on the Agent's disk and the old Runtime no longer expires them. Do not silently delete those owner files during rollback.

Blast Radius: dashboard/runtime

Every Agent navigation gains Export; downloads use dashboard bandwidth and Agent disk. A Runtime image rollout is required for the plugin. This is content portability, not identity/key backup or a Recovery Set. The move-site copy and destination upload/import page are outside this PR, and no destination-import compatibility is claimed. Agents that cannot start cannot use this path.

Contract: Agent export.

AustinKelsay and others added 3 commits October 29, 2026 23:17
Copy https://move.finite.place/export.py byte for byte (sha256
29021b5325280f4db80fc407047b932edaabe1f8626042ba957131ea4bc39563,
fetched 2026-10-29) so the dashboard export and the move guide run one
exporter. Later commits format it and lift the chat-attachment limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mechanical only: `ruff format` and `ruff check --fix` (datetime.UTC,
unused noqa). No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The move guide sends its zip as a chat attachment, so the exporter drops
media and binaries and any file over 5 MiB. A download has no such
limit: `survey --everything` and `build --everything` keep both, while
credentials, dependencies and build output stay out as before.

Archives and installers still stay out of the workspace, because the
files inside them can't be checked for credentials. The credential scan
now reads text files of any size in chunks and carries each unfinished
word into the next chunk, so no match is split; a single line over
16 MiB can't be checked and keeps its file out. This also tightens the
move guide's export, which left text files over 2 MiB unread.

Media, archives and binaries are stored in the zip rather than deflated
again, and the workspace never includes an earlier export folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Documentation census

This PR adds 1 new Markdown file and 73 lines of Markdown, and removes 0 Markdown files and 0 lines of Markdown.

Net documentation change: +73 lines.

Counts .md and .markdown files anywhere in the repo, case-insensitively. Lines include additions and deletions across all matching files; net change is additions minus deletions. Renamed files are not counted as new or removed. Advisory only; never a merge gate.

AustinKelsay and others added 3 commits October 29, 2026 23:49
Add a finite-export Hermes dashboard plugin, packaged beside the Brain
and Sites plugins. The owner's native Hermes grant starts a survey, then
an export with the brains and sites they choose; the exporter runs in
the background as its own process, never through the model or chat.

The job's folder under the Finite Home records how it ended, so a Hermes
restart cannot lose a finished export or report a dead one as running.
One job runs at a time, a new export replaces the last, and a finished
one is deleted after seven days. The download is a plain file response,
so Range requests can resume it.

The disk is shared with the host's other Agents. An export starts only
when 20 GiB would stay free after its staging copy and zip, and a running
export is stopped, and its staging copy removed, if free space falls
below 10 GiB: the survey can't size sites or brains not yet on the Agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent navigation gains Export. The page asks the agent's finite-export
plugin what it has, lets the person pick brains and sites (their own
brains and owned sites start chosen), starts the export, checks in every
three seconds while it runs, and offers Download and Delete.

The download is a same-origin route: the dashboard gets a grant from
Core, fetches the zip from the agent and streams it through, forwarding
only a single well-formed Range and the exporter's own file name.
Requests from other sites are refused. Typing /export in web chat opens
the page instead of reaching the agent; Hermes' own /export is CLI-only.

The design fixture can now issue the native Hermes grant to a test's
stand-in agent, and the browser test drives the flow end to end over
HTTPS with a throwaway certificate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Who may export, where its state lives, its limits, what the zip holds
and leaves out, and why it needs a Runtime rollout but is no backup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AustinKelsay
AustinKelsay force-pushed the feat/agent-export-20261029 branch from 28b26c2 to ce2d618 Compare October 30, 2026 04:49

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail for now. The dashboard flow works, but the export's credential and shared-disk guarantees have reachable gaps at ce2d61801ba60eca66f1f14e6fb6fb35c55a3d75.

  1. [P1] Apply the credential exclusion policy to every exported section. export.py:1443–1452 copies cron JSON and serializes prompts/output without filtering; export_memories also snapshots memory_store.db verbatim. Independently running build --everything on disposable fixtures preserved a known-pattern synthetic GitHub token in both cron files and the memory DB. The same token survived a UTF-16 workspace text file because secret_in_text returns safe at the first NUL, and a compressed custom-skill archive because archive exclusion only applies to the workspace. These contradict the page/README's “passwords, keys and tokens are never included” promise and can move credentials to the destination importer. Cover these paths with a consistent filtering/redaction or exclusion policy and regression tests that inspect decoded/decompressed archive members. Any intentionally unfiltered content, including chats, must be explicitly disclosed instead of promising a credential-free zip.

  2. [P1] Include surveys in shared-disk protection. plugin_api.py:197–201 watches only export, and _start_survey has no space admission check. A survey copies the entire state.db into its scratch directory (survey_chats → open_sessions → snapshot_sqlite). With a large chat DB and insufficient remaining disk, simply clicking “Check what to export” can exhaust the disk shared by Agents before export admission runs. With free space mocked to zero, I observed survey admission return 202 and guard() leave its process alive. Apply admission/watch/cleanup to this disk-writing phase too; test low space before and during a survey.

  3. [P2] Stop the spawned process when persisting its job record fails. plugin_api.py:164–174 starts the process before writing/replacing job.json, without failure cleanup. Injecting ENOSPC at job.json.part left the child alive, busy() false, and DELETE unable to kill it. Subsequent requests can start overlapping jobs while the original remains outside the disk watcher. On any post-spawn setup failure, kill and reap the process group and clean its partial state; cover both record write and rename failures.

Validation: 458 dashboard tests passed, one unrelated Brain deletion proof skipped on the preceding head; the delta only changes the Python contract test’s process-reaping assertion. The real dashboard browser test passed with synthetic account/Core/hosted-device fixtures and an HTTPS stand-in Agent; an independent extension passed repeated exports, cancellation, leave/revisit, and failed/interrupted recovery at 390×844 and 320×844, with no horizontal overflow. Desktop viewport: 1440×1000. Download bytes, Range, cross-site rejection, deletion and /export navigation passed. Python reproductions used the real exporter/plugin source with synthetic files, mocked CLI discovery/disk values, and disposable sleeper processes for lifecycle failures.

Current-head CI is still running. The preceding head failed the Packaged Agent export contract; the new commit changes its Linux process-reaping assertion. All three reproductions and the extended browser flow were rerun on the current head. Direct plugin checks also passed download/suffix Range/416, input validation, outside-path and symlink refusal, deletion and expiration. Local native-Hermes contract tests stopped before executing (hermes_state missing); Nix/package execution remains unavailable. Browser fixtures do not prove native Hermes authentication, packaging, or live Core integration. Screenshots were captured and inspected locally; authorized Drive publication remains blocked by unverified sharing access, so no inline image claim is made.

AustinKelsay and others added 2 commits October 30, 2026 01:01
Review of #1136 found the credential promise held only for the workspace,
and the shared-disk rules only for exports.

Credentials: the exporter now handles them the same way everywhere.
Scheduled jobs are redacted like memories, string by string in the
decoded JSON (in serialized JSON an escape such as \n hides a token's
start), and redaction removes a whole private key, not only its header.

A SQLite database, memory_store.db included, goes in only as a copy
rebuilt from its live rows with VACUUM INTO and checked value by value,
so no deleted rows, free pages or write-ahead log come along; its -wal,
-shm and -journal files stay out. Its read-only SQLite URI is escaped,
so `#` or `?` in a file name can't open another file.

Every other file is read as UTF-8, or UTF-16 with a byte order mark;
archives stay out in every section, skills and sites included. Chats and
media go as they are, and the page, the zip's README and the doc now say
so instead of promising a credential-free zip.

Disk: a survey copies the chat store, so it now starts only with room
for that plus the 20 GiB reserve, and the low-disk guard stops a running
survey as well as an export. A job whose record can't be written or
renamed is killed and reaped at once instead of running untracked, and
a full disk answers notEnoughSpace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row actions only take pointer events while the row is hovered or
focused. The agent creation browser test clicked Archive without
hovering, and passed only because Playwright's retry scrolling happened
to bring the row under the resting mouse. One more agent navigation item
(Export) moves the row, and the click then times out. Hover first, as a
person does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AustinKelsay

Copy link
Copy Markdown
Contributor Author

Note

🤖 Claude Opus 5.5 responding on behalf of AustinKelsay

@alexlwn123 Thanks. All three findings reproduced, and all three are fixed in 69aec86, now at head 1576682. Each fix has a regression test that fails without it; I checked that by breaking the code on scratch copies.

  1. [P1] Credentials in every section.
    • Cron: cron/jobs.json and cron/index.json are now redacted value by value in the decoded JSON (redact_values), so a token after an escaped \r\n or \t is still caught. Redaction also removes whole PEM private-key blocks, not only the header.
    • Databases: memory_store.db, and every other SQLite database, goes in only as a VACUUM INTO rebuild of its live rows, checked value by value (compact_database, secret_in_database). Deleted rows, free pages and WAL contents don't come along. -wal, -shm and -journal files stay out, and the read-only URI is escaped, so # or ? in a file name can't open another file.
    • UTF-16: text with a byte order mark is decoded and scanned like UTF-8 (secret_in_file).
    • Archives: they stay out in every section (archive_reason in leave_out_reason and asset_refusal), which covers the compressed skill archive.
    • Disclosure: chats and media go as they are, and the page, the zip README and docs/agent-export.md now say so instead of promising a credential-free zip. Other binary files, and UTF-16 without a byte order mark, aren't read; that's disclosed too.
    • Test: infra/images/test_hermes_agent_export.py plants credentials in each of these paths. It checks every zip member after opening nested archives, UTF-16 text and SQLite values, and checks that clean UTF-16 and SQLite files are still exported.
  2. [P1] Surveys and the shared disk. _start_survey now requires free space for the chat store (state.db plus -wal) plus the 20 GiB reserve. guard() stops a running survey as well as an export. Tests cover low space before a survey (507) and during one (failed, process gone).
  3. [P2] Untracked job. If writing or renaming job.json.part fails, start() kills and reaps the process group, removes the folder and re-raises. ENOSPC answers 507 notEnoughSpace. The test injects ENOSPC on both the write and the rename, then checks that the child was reaped and nothing is left running.

Also in this push, 1576682 makes agent-creation.browser.ts hover the chat row before clicking its hover-only Archive action. That fixes the previous head's Dashboard failure: the new Export nav item moved the row out from under the resting mouse.

The native-Hermes contract test passes 8/8 against the plugin as packaged by nix build .#packages.aarch64-darwin.hermes-agent-runtime. Linux CI for this head is running.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail for now — the prior reproductions are fixed, but the new SQLite credential check has one remaining leak. Reviewed 1576682a9129b11a3718aa79a820c5e4c4039dc3.

[P2] Check schema SQL before accepting a rebuilt database. export.py:418–426 enumerates user tables and scans their rows, but never scans sqlite_master.sql. VACUUM INTO retains column defaults, views and triggers. An otherwise empty app.db with a credential-shaped synthetic GitHub token in a column default or view therefore returns None from secret_in_database and is included by the real build --everything path. I restored the exported member and recovered the token through both SELECT from the view and an insert using the column default. This leaves the credential-exclusion contract incomplete even though live-row and deleted-page cases now pass. Scan the persisted schema SQL as well as table values, reject the database on a match, and add default/view/trigger fixtures that inspect the exported database.

Independently verified resolution of the previously reported concrete cases: cron JSON and whole private keys are redacted; live secret-bearing memory databases, BOM-marked UTF-16 credentials and skill archives are excluded; deleted SQLite bytes and stale WAL credentials do not survive; clean #, ? and % filenames resolve correctly; row IDs and an external-content FTS query survive VACUUM INTO. Survey admission counts DB+WAL, its low-disk guard kills the worker and removes staging, and both job-record write and rename ENOSPC failures return 507 with the worker killed/reaped and partial state removed.

Validation: nine focused dashboard tests and both tests in the complete agent-creation.browser.ts suite passed, including its updated hover-before-archive path. Real-dashboard browser checks passed at 1440×1000, 390×844 and 320×844, including repeated exports, cancel/revisit, interrupted/failed recovery, download/Range, and /export; no horizontal overflow. The changed notice is readable on mobile. I traced the archive action's hover/focus CSS and independently exercised hover → archive → restore through the real sidebar. Account/Core/hosted-device services and the HTTPS Agent endpoint were synthetic fixtures; native Hermes/authentication and packaging were not exercised locally. The current packaged Agent export CI step passes, per GitHub step metadata. Local native contract execution remains unavailable (hermes_state/hermes_cli missing). Screenshots are inspected and retained locally; Drive publication remains blocked by the previously unverified sharing access. No Nix or CI-log denial was retried.

VACUUM INTO keeps a database's schema, so a credential in a column
default, a view or a trigger survived the rebuilt copy: the check read
only table rows. It now reads every sqlite_master SQL statement first
and leaves the database out on a match. Review of #1136 found it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AustinKelsay

Copy link
Copy Markdown
Contributor Author

Note

🤖 Claude Opus 5.5 responding on behalf of AustinKelsay

@alexlwn123 Agreed: VACUUM INTO keeps the schema, and the check read only table rows. Fixed in d7fcc45, now at head d7fcc45.

  • secret_in_database first reads every sqlite_master SQL statement, then the table values. The database stays out on a match, with the reason "looks like it holds a secret".
  • The test fixture adds three databases with no rows, each holding a credential only in its schema: a column default (defaults.db), a view (views.db) and a trigger (triggers.db). Each is now skipped. The zip check now also reads sqlite_master SQL from any exported database.
  • Removing the schema scan, which is the code at 1576682, fails the test: the column-default token appears in the export.

The contract test passes 8/8 in source mode and against the packaged plugin. A real-dashboard run against the real Hermes web server and the packaged plugin exported 26 files, with none of 20 planted credentials found after opening every member, nested archive, UTF-16 file and database.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass for the reviewed fixes. At d7fcc45408a238e8c2eeeb19b0857385ce58a656, the schema scan resolves my remaining SQLite finding; I found no new blocker in this delta. Posting a COMMENT, not an approval.

Independently reran the original combined default/view reproduction plus separate credential-bearing defaults, views and triggers. The scanner rejects all four databases; compact_database removes rejected copies; the real build --everything path excludes them and records the reason in the manifest. Clean equivalents remain included and their defaults, views and triggers work after restoring the exported members. Exported member bytes contain none of the synthetic token, #/? filenames work, and source rows remain unchanged.

Also reran the prior credential, deleted-page/live-WAL, escaped-URI, row-ID/external-content FTS, survey admission/watchdog, and job-record write/rename ENOSPC checks successfully. These execute the actual exporter/plugin source with disposable files and processes; CLI discovery and disk capacity are mocked. No customer data was used.

Coverage limits: native Hermes dependencies (hermes_state, hermes_cli) are absent locally, so this is not packaged-runtime/authentication integration coverage. Current-head CI is still running. This delta changes only the exporter and its Python contract test; desktop/mobile export and hover→archive→restore browser coverage remains the prior execution at 1576682a9129b11a3718aa79a820c5e4c4039dc3, not a rerun on this head. No screenshots were published, and prior Nix, CI-log and Drive-access restrictions were not retried.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass for this follow-up. No new findings at 815a3c060a8d7b733d7bf94ba4233514a94ca14f. The delta from d7fcc454 changes only the Python contract fixtures and compatibility documentation; production code and UI are unchanged.

Independently exercised the added filename collision cases through the real build --everything path: clean keys.db?draft retains its own rows even though keys.db contains a synthetic credential; credential-bearing plants.db?keys is excluded even though plants.db is clean. Manifest reasons, absence of leaked token bytes, and absence of stray source files passed. The prior schema/default/view/trigger, credential, WAL/URI, survey disk-limit and failed-job-persistence checks also passed again.

18 existing focused dashboard tests passed, plus an independent mocked-404 test through readExportStatus confirming the exact documented message: “This agent does not support this page yet.” Python checks used disposable data and real exporter/plugin code, with CLI discovery and disk capacity mocked. No customer exports or external service integration was involved.

Native Hermes/package tests remain unavailable locally (hermes_state/hermes_cli absent); current-head CI is still running. No browser rerun was needed for this test/docs-only delta: prior desktop/mobile evidence belongs to 1576682a9129b11a3718aa79a820c5e4c4039dc3. Prior Nix/CI-log/Drive restrictions were not retried, and no screenshots were published. Submitted as COMMENT, not approval.

@AustinKelsay
AustinKelsay dismissed stale reviews from alexlwn123 and alexlwn123 October 30, 2026 15:26

Superseded findings verified fixed, not waived. Alex independently rechecked the original credential, survey-disk and failed-job-persistence cases and posted passes at d7fcc45 (review 5708755342) and current 815a3c0 (review 5714359243). Current-head CI run41077983130 passes all15jobs; independent Fable5.1 source/synthetic review of815a3c06 and retained34b30e29 approves, including sealed8/8tests and8caughtmutants. Maintainer reconciliation for the user-authorized release; this does not represent an Alex approving review or production qualification. Normal required checks and merge queue remain in force.

@AustinKelsay
AustinKelsay added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 30, 2026
@AustinKelsay
AustinKelsay added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 4c5e49a Oct 30, 2026
22 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants