c1177a934d3bc2d7e41f88adedf075fbe93777c8
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4f10528368 |
feat: conversation review — a nightly report on a supervised person's conversations
Nightly Build / build (push) Successful in 7m40s
The first AgentScope::PerSubject system agent, and the reason that scope exists. Once a night, for each person with a supervision edge, it reads every message that person and the assistant exchanged since the previous review — across all their conversations — and writes one report for the people who supervise them. Schema (all registry except reports): - supervision(subject_user_id, supervisor_user_id): the generic §0.1 edge, answering both 'whom does a background agent look at' and 'who may read what it produced', with real FKs so deleting a user cascades both ways - system_agent_coverage(agent_id, subject_user_id, covered_through): the per-subject watermark that makes 'everything since last time' a window — neither system_agent_runs (history for humans) nor system_agent_state (advances before the work), and advanced only on a completed pass so a crash re-covers instead of skipping - reports (owner schema, the second two-homes table after memory_docs): instance rows land in system.db, deliberately cleartext to the box owner, who is the intended reader (§2); the subject cannot see them structurally The pass reads the subject's database inside a supervisor's runtime, so the ephemeral session and run row land in the watcher's file; iteration is over subjects, so two parents watching one child get one review; and the subject need not be logged in when their space is unencrypted — via the new UserManager::open_unencrypted, which refuses an encrypted user outright (no key to be had) and never registers the pool as unlocked. The agent declares the new AgentMeta flag allow_tools: false, so its turn gets an empty tool registry — nothing for a prompt injection in the transcript to call — and produces its report as its final assistant message, read back from chat_history and parsed (NOTHING_TO_REPORT sentinel, no row on quiet days). chat_history::conversation_window is the transcript query; its four filters (non-ephemeral, depth 0, non-synthetic, non-empty) each guard a specific way the review would otherwise be wrong, and tool calls are absent by construction. Cadence is Run at (hour) rather than Interval — 4am local by default — with due-ness answered inside has_work against the coverage watermark, so a machine off for three days covers the whole stretch in one pass. Reports announce ReportCreated on the system bus (no subscriber yet). run_ephemeral_turn gains a per-pass system_substitutions map, which the review uses to hand the model the subject's profile under __SUBJECT_PROFILE__ — the system-context substitutions describe the session owner, the wrong person here. docs/system-agents.md gains the conversation review section; CLAUDE.md documents the scope, the tables and the tool-less design. |
||
|
|
305bdbdd2b |
connectors: announce global-server and reinstall refreshes on the bus
Nightly Build / build (push) Successful in 6m56s
Five call-sites reached into the live-runtime refresh helpers from HTTP handlers, the same shape as the container remounts. Only three of them belonged on the bus, and finding out which was the point. global_enable and global_delete now emit McpGlobalServersChanged, and the marketplace reinstall emits ConnectorReinstalled. All three are pure reconciliation: the first only makes a connector appear; the second is already enforced by stop_server, with the snapshot refresh just tidying each user's filter; the third pushes metadata and code into what is already running. The reinstall gains something from being off the response path, since it re-copies files and restarts servers inside every live user's container. global_set_access and user_connectors_set keep calling refresh_global_mcp_access directly. Their writes *replace* a grant set, so anyone dropped from the list is being revoked and that refresh is what enforces it — on a best-effort broadcast a revoked user would keep the connector until their next login. Both carry a DELIBERATELY SYNCHRONOUS comment, since they are otherwise indistinguishable from the announced call-sites and are exactly what a later cleanup would sweep up. No behaviour change for the two synchronous paths; the three announced ones now return without waiting for the refresh. |
||
|
|
0ba140186f |
auth: make deactivation and group revocation actually revoke
Nightly Build / build (push) Successful in 6m58s
Two variants of the same defect: an admin took away access and the running system kept granting it. Deactivating or deleting a user only stopped the *next* login. `login` checks the active flag, but `require_auth` maps token -> id without re-reading the row, so an already-open session kept working over a pool whose key was still in RAM. There was no way to stop one user either: the per-user cron, hub and MCP loops all observed the *instance* shutdown token. They now take a per-user child token stored on UserContext, and Skald::revoke_user_runtime tears a single user down in a load-bearing order — revoke every session, evict and cancel the context, then lock the database, so nothing is left querying a pool we are about to close. Revoking a security group had a durable version of the same problem. The group is validated when selected and then persisted on chat_sessions.run_context, which was replayed verbatim on every later load — so a group removed from a role stayed in force on sessions that already had it, across restarts. get_or_create_handler now runs the stored value through run_context::reconcile_group_for_user, making it advisory: every load re-checks it, whether or not anyone announced the change. The degrade target is the role's default group, never None: a context with no group resolves to the catch-all `default`, whose rules are the fallback tier under every other group, so clearing widens rather than narrows. The reconcile touches only security_group, so a project session's server-built project_root and system_prompt survive a permissions edit, and it leaves the stored group alone when the role cannot be resolved — guessing on a transient error could only widen. role_default_run_context moves into the core seam so the group a session starts on and the group it falls back to cannot drift apart. Both fixes run synchronously in their handlers. Only the container half of deactivation rides the bus, as the new UserActiveChanged event: a lossy 64-slot broadcast whose contract is "settles at the next login" is the wrong transport for taking access away. Tests: revoke_user drops all of one user's sessions and nobody else's, and is a no-op when nothing is live; the reconcile degrades a revoked group to the role default, keeps an allowed one, preserves project fields in both directions, never touches an admin, and stays put when the role is unresolvable. Not exercised at runtime: no Docker/live-server run, so the end-to-end paths (deactivating a logged-in user, editing a role with sessions open) are covered by unit tests only. |
||
|
|
c50a0d84da |
containers: drive user provisioning and remounts from the system bus
Nightly Build / build (push) Successful in 6m51s
The endpoints that changed a user or a membership row also reached into ContainerManager themselves: users_mgmt called ensure()/remove(), and both shared_folders and projects called refresh_user_mounts through a local remount() helper. Every future endpoint that grants membership would have had to remember to do the same. Announce instead. SystemEventBus gains UserCreated / UserDeleted / UserMountsChanged, emitted after the DB write, and one subscriber — wiring::spawn_user_lifecycle — does the Docker work: sequentially (which serialises concurrent operations on the same container), best-effort by contract (the row is already committed, so a hiccup settles at the user's next login or at boot reconciliation), and holding only a Weak<Skald>. It is spawned after construction, like set_skald, because it reacts through Skald's own accessors. Also fixes a real gap the event makes impossible to repeat: the web setup wizard created the first admin without provisioning a container. It runs against a live server, where reconcile_all() has already happened, so that admin had no sandbox until the next restart. It now emits UserCreated like any other creator; the console shell needs no equivalent, since it runs before the server and boot reconciliation covers it. Two behaviour changes: POST /api/users and POST /api/projects no longer wait on Docker before responding. Provisioning was already best-effort, and a new project's folder is still created synchronously, so the explorer — which reads host-side — shows it at once; only execute_cmd reachability lands a moment later. |
||
|
|
38494a85a9 | First Version |