fix(webui): prevent skill cards from overflowing grid tracks - #1
Open
Preciousuche wants to merge 94 commits into
Open
fix(webui): prevent skill cards from overflowing grid tracks#1Preciousuche wants to merge 94 commits into
fix(webui): prevent skill cards from overflowing grid tracks#1Preciousuche wants to merge 94 commits into
Conversation
…ine-breaks fix(cli): wrap onboarding prompt input
Restore embedded Config layout and YAML sizing, use the Bankr brand asset when catalog metadata omits a logo, and add an explicit no-backup reset recovery action shared by chat controls. Refs use-agent-os#82, use-agent-os#84, use-agent-os#85
fix(frontend): repair settings, Bankr icons, and session reset
…ark latency ceiling
…atibility fix(platform): resolve windows compatibility type-checking and test s…
…7.25 chore(release): bump version to 2026.7.25
Use the gateway route channel metadata as the Telegram chat ID and map a thread-targeted reply_to value to message_thread_id. Add an end-to-end regression test from an inbound forum command through the Bot API payload. Fixes use-agent-os#50
…-gateway feat(provider): add OpenCAP gateway support
…ive-command-mentions-51 fix(telegram): handle native bot command mentions
…m-command-replies fix(telegram): preserve forum command reply targets
Acknowledge application commands before dispatch and complete deferred original responses for command, batch, and streaming reply paths.
Gracefully degrade Discord native command registration failures without aborting gateway startup. Adds regression coverage for REST errors and exhausted rate-limit retries.\n\nFixes use-agent-os#54
Derive Slack conversation types from channel ID prefixes and mark native slash forms as explicit interactions. Preserve group access controls while bypassing mention-only gating for slash commands. Refs use-agent-os#56
Grant operator.read to channel senders after access-policy admission while reserving operator.write for configured channel admins. Document the permission model and cover read/write authorization tiers. Refs use-agent-os#57
…nt (use-agent-os#127) * feat(env): add a policy-gated writer for ~/.agentos/.env AgentOS could read .env files but never write one, so every flow that detected a missing environment variable could only tell the operator to go edit a dotfile by hand. This lands the write side. env_policy holds the gate. Writing an environment variable is not neutral: every subprocess inherits os.environ verbatim, and several AgentOS behaviours (sandbox guards, the shell allowlist, the gateway token) are themselves read from the environment. Names that steer subprocess execution or runtime posture are refused on write, name by name, so the writable surface cannot escalate its own privileges. The AGENTOS_ prefix is not blanket-blocked — ordinary credentials live there too. env_store holds the I/O. Values round-trip through the existing reader rather than requiring new escape semantics, so hand-written files keep parsing exactly as they did; the quoting rules cover the three cases that would otherwise lose data (empty, edge whitespace, self-quoted). Writes are atomic and preserve an existing file's mode, because deployments that bind-mount .env at 0640 should not have it tightened behind their back. Line breaks in values are refused rather than stripped. Silently truncating a pasted multi-line credential turns into a mysterious 401 hours later. env.py's parser now recognises `export KEY=` as well. Missing that form means a save appends a second definition and a later unset resurrects the old value. Precedence is unchanged: os.environ still wins over the file. resolve_entry reports which source a value actually came from so surfaces can warn when a freshly written value is being shadowed, and get_env_value_prefer_file lets a mid-session rotation win where that matters. * refactor(migration): write migrated .env through the shared env writer Both migrators hand-rolled their own .env serialization. Routing them through env_store gives the migrated file the same atomic replace, quoting, and permissions as anything AgentOS writes later, and removes the duplicate implementation. Two bugs go with it: The Hermes migrator wrote the file at the default umask, so credentials imported from a prior install were world-readable on a typical box. They now land at 0600 like every other .env AgentOS creates. Values with significant leading or trailing whitespace were written bare and then eaten by the reader's strip(). The OpenClaw migrator moves a command allowlist across, and entries in it are prefix patterns — "^pytest " with the trailing space matches the command, "^pytest" without it matches anything starting with those six characters. Migrating an allowlist and silently widening it is exactly the wrong direction. The two tests covering this asserted the lossy serialization, so they now assert the parsed value instead: what the allowlist means, not how it is spelled on disk. The denylist is deliberately not enforced on this path. Importing an operator's own prior configuration is equivalent to them editing the file by hand; refusing the settings they already had would be a worse outcome than migrating them. Name and value validation still applies. * feat(skills): let manifests describe the env vars they need `requires.env` was a list of bare names, so every surface that reported a missing variable could only print the name. An operator seeing "Missing env var: BASE_RPC_URL" still has to work out what it is, whether it is a secret, and where a value comes from. Manifests may now declare the richer form: requires: env: - name: BASE_RPC_URL description: Base L2 RPC endpoint url: https://docs.base.org/ secret: false Plain string lists keep working — SkillEnvVar.coerce upgrades them — so no existing manifest changes, and a malformed entry is dropped rather than failing the whole skill load. Coercion lives in __post_init__ because manifests, the on-disk cache, and tests all build SkillRequires from raw data, and one of them forgetting would silently produce strings where callers expect structured entries. The cache stores the dict form for the same reason: serializing names only would mean a cache hit quietly downgrades what surfaces can say. EligibilityReport gains missing_env_detail alongside the existing missing_env list, which keeps its shape for current callers. skills.list carries the detail through, and skill_list's hint now names what the variable is for, links where to get one, and points at the tool that can set it instead of describing work for the user to go do by hand. env_catalog assembles all of this into one description of every variable AgentOS reads. Provider keys are derived from the five onboarding spec families rather than re-listed, so adding a provider cannot leave the catalog stale; skills contribute their own declarations; and names found in the user's .env that nothing declares are surfaced as custom rather than hidden. Entries know whether a change needs a restart — a provider client is built once at boot, a skill's variable is read by a process spawned after the change. * feat(gateway): add env.list/set/unset/reveal RPC The control surface can now read the state of every environment variable AgentOS depends on and change the ones it is allowed to change. Listing never carries a value. env.list reports whether a variable is set, where its value is coming from, what it is for, and who needs it — but the value itself only ever appears as a mask. Same for set and unset, which return the resulting state rather than echoing what was written. That keeps the ordinary browsing path free of secrets, so a screenshot or a log line cannot leak one, and the tests assert it against the whole serialized payload rather than the one field we remembered to check. env.reveal exists because operators do need to see a value sometimes — confirming which of two keys is installed, copying one elsewhere — but it is a separate method with its own cost: five per thirty seconds, process wide so a second tab does not reset the budget, and an audit line that records the name and never the value. Enough for the human case, useless for a bulk export. Writes go through the same policy gate as every other surface, and the refusal message is passed through verbatim because it explains which class of name was refused and what to do instead. The response carries restartRequired per variable rather than a blanket answer: a provider client was built at boot with the key it had then, while a skill's variable is read by a process spawned after the change. The listing also counts variables shadowed by the process environment, which is what turns "I saved it and nothing happened" into something the UI can explain. * feat(cli): add `agentos env` for managing environment variables `agentos config` edits the TOML config; there was no equivalent for the .env file, so the answer to "OPENAI_API_KEY is not visible to this gateway" was always to go edit a dotfile by hand. The command prefers the running gateway, which is what makes a change apply to the live process — a skill that was ineligible for want of a variable becomes eligible without a restart. When no gateway is running it writes the file directly and says the value applies at next start, because the first thing a fresh install needs is a way to set a provider key before anything can start. The fallback is never silent about being a fallback. Values are never printed unless asked for. `list` and `get` show a mask; `get --reveal` prompts before putting a secret on the terminal. `--stdin` and the interactive prompt are the documented ways to supply a value, since a `--value` flag lands in shell history and in the process list — the flag still exists for non-secrets and scripts that have already solved that problem. Listings say which skill or provider needs each variable, and warn when a value is being shadowed by the process environment. Without that warning an operator edits the file, sees nothing change, and has no way to find out why. Provider keys are not marked "missing" when unset. AgentOS talks to one provider at a time, so flagging all forty as missing would bury the one that actually needs attention; onboarding status already reports whether the configured provider has its key. docs/cli.md and the bundled agentos skill document the new surface, per the rule that a CLI change updates both in the same commit. * feat(tools): let the agent report and (with approval) set env vars skill_list could already say that a skill needs BASE_RPC_URL; nothing could act on it, so the model's only move was to hand the user a homework assignment. These two tools close that, with the asymmetry the risk warrants. env_list is exposed by default and carries no values — not even masked ones. Diagnosing "this skill is unavailable because X is missing" needs the name; it never needs the secret, and there is no reason to put one in a transcript that gets replayed, exported, and fed back to a model. env_set is hidden by default and gated behind the approval queue, the same way a patch or a warnlisted command is. A denylisted name is refused before any approval is requested: an operator should not be offered the chance to wave through a sandbox escape by clicking approve. The approval record describes the write without carrying the value, and is bound to the variable it was granted for, so a grant for a harmless name cannot be replayed to write a different one. There is deliberately no reveal tool, and a test asserts none appears. A model that can read back stored credentials is one prompt injection away from exfiltrating them, and nothing an agent legitimately does requires it. * feat(web-ui): add an Environment screen and fix env vars where they break A missing environment variable was visible in three places and fixable in none of them. This adds the screen where they are managed, and an action in the one dialog that already diagnosed the problem. /env is its own view, alongside Skills and MCP rather than buried in Settings. Variables are grouped by what needs them — provider, search, memory, each skill, and the operator's own — because "OPENAI_API_KEY is not set" is only actionable once you know what reads it. Rows carry the description and, where a manifest declared one, a link to where the credential comes from. Values are masked. Reveal asks first and hides again after thirty seconds, since a value left on screen outlives the moment someone needed it and ends up in a screen recording. Variables the server refuses to write render locked with the reason, rather than silently ignoring a save. The shadowed-variable warning is the part that earns its screen space: when the shell that started the gateway exported a variable, editing the file changes nothing, and without being told an operator will save repeatedly and conclude the feature is broken. Settings gets a one-line pointer rather than a second copy of the table — one place to look, one query cache shared between the two. The Skills dialog gains a "Set <VAR>" button next to the existing "Install via <kind>": same dialog, two classes of dependency, and until now only one of them was fixable. * feat(skills): let skills declare non-secret settings in the config Skills need two different kinds of value from an operator, and treating both as credentials makes both worse. An API key belongs in .env, where it can be masked, gated, and audited. A wiki directory or an output format has nothing to hide, and hiding it only makes the setup harder to inspect, diff, and share. A manifest can now declare the second kind: metadata: agentos: config: - key: wiki.path description: Path to the knowledge base directory default: "~/wiki" Values live in the TOML config under [skills.config], written through the existing config.set — no new RPC for something the config surface already does. The section is free-form because the keys belong to the skills; a fixed schema would mean this file had to know about every skill anyone installs. skill_view appends the values in effect, so the agent starts from what is configured rather than asking the user or reading the config file itself. Skills that declare nothing add nothing to the output, and any failure to build the block yields an empty string — a missing config line is a smaller problem than a skill that will not open. Defaults are honoured when resolving, and "~" is expanded, because these settings are overwhelmingly paths and an unexpanded ~/wiki is a directory literally named "~". A declaration that has a usable default is not reported as missing: the skill works out of the box. The snapshot schema version goes to 9. Both this change and the richer requires.env from the previous commit alter what the cache stores, and a version-8 snapshot would load without error while silently dropping them. * docs(env): document the environment surface and register its import edges configuration.md gets the two things an operator cannot work out from the UI: the load order that makes a shell export win over the file (and how to tell when that is happening), and the list of names AgentOS refuses to write, with why. web-ui.md describes the Environment screen and the Skills-dialog action; http-api.md lists the env.* methods and the properties that matter — no values in a listing, reveal rate limited and audited, restart required per variable. agentos.toml.example shows [skills.config] with the boundary spelled out: settings here, credentials in .env. The architecture contract gains env_catalog's two edges rather than a suppression. Provider keys come from the onboarding specs that already declare them and skill variables from loaded manifests, so the catalog stays correct when either is added; a hand-kept list is exactly what would drift. Both imports are lazy, so nothing loads until a surface asks for a listing. The release-hygiene test caught a "/home/<user>/" shaped path in a frontend fixture. Replaced with ~/.agentos/.env, which is what the UI shows anyway. * fix(cli): keep --json output free of startup log lines The CLI loads .env files before anything else, and those loads log. With structlog's unconfigured default that output goes to stdout, so every --json payload arrived with log lines in front of it the moment a user had a populated .env: `agentos env list --json | jq` worked on a clean machine and failed on a real one. Logs are diagnostics; stdout belongs to the command's output. The stream is resolved per write rather than captured once. Handing structlog `sys.stderr` directly freezes whichever object is installed at import time, and anything that replaces it afterwards — pytest's capture, a TUI taking over the terminal, a caller redirecting for one command — would be written past. The first attempt did exactly that and broke a test asserting on captured output, which is the same way it would have failed a user. Predates the env work but surfaced through it, since --json is now documented as machine-readable. * fix(web-ui): give the Environment screen the shared header and a scannable list Live review turned up three problems, one of them structural. control-surface.css grants the shared stage-header treatment through explicit :is() allowlists, and this view had never joined them — so it rendered an ad-hoc header with different height, padding, and alignment from every sibling page. Adding .env-stage__header, __title-block, and __actions to those lists and deleting the local copies is the fix; a contract test now asserts the view stays on every allowlist MCP is on, since drifting off one is invisible until someone looks at two pages side by side. The list buried what mattered. AgentOS declares ~22 LLM provider keys and an install uses one, so 21 empty rows sat above the two variables that needed attention. Rows that are set or required-and-missing now show; the rest fold behind "Show N unset" with the group's full counts still stated. A fresh install goes from a wall of "unset" to a page that fits on one screen. Every row also repeated its own name inside its button ("Set AIHUBMIX_API_KEY" x22). The visible label is now "Set"/"Edit" with the variable in the accessible name, so screen readers keep the distinction the eye already gets from the row. Smaller things from the same pass: counts and the file path move out of the title block into their own strip, and the path is shortened to its last two segments with the full value on hover — CSS truncation either ate the filename or, with the direction:rtl trick, relocated the leading slash. Also fixes a real defect found while testing: the confirmation modal's panel was styled under `.control-surface`, which never matches, because ModalShell portals into document.body — the panel rendered with no width or background. Skills scopes its own modal unscoped for the same reason. * fix(web-ui): space the Environment header buttons and add variables in a dialog Two things from a second look at the running page. Refresh and Add variable were touching, with no gap at all. Moving the header onto the shared control-surface treatment, I also deleted the local .env-stage__actions rule on the assumption the shared stylesheet supplied it. It does not: the allowlist there only tunes the actions row responsively, and every sibling view declares the base flex row itself. Restored to match .mcp-stage__actions, and the CSS contract test now asserts the row exists rather than asserting it is absent. Add variable opened an inline form wedged between the header and the toolbar, which read as part of the page rather than as something you had started. It is a dialog now, like adding an agent, a channel, or an MCP server. The dialog stays open when a write is refused and shows what the server said, instead of closing and discarding what was typed — the refusal message names the class of variable and what to do instead, which is worth reading. Opening it always starts blank, so a name abandoned earlier cannot reappear in a later save. * fix(web-ui): stop the Environment view swapping itself out while loading The loading state replaced the entire page — header included — with three grey slabs, so every visit rendered the tall shared header band, tore it down for the skeleton, and built it again when the data landed. The page appeared to break and reassemble on each navigation. The header, counts strip, and toolbar now stay mounted throughout; only the list area says it is still fetching. Nothing moves when the response arrives, which is also what the layout-stability rule asks for. The skeleton markup and its styles are gone, and a test asserts they do not come back. The refresh button keeps its spinner: it is scoped to the control the operator just pressed, and every sibling view does the same. * fix(config): do not write an empty [skills.config] on upgrade Every nested config model forbids unknown keys, so a config file carrying `[skills.config]` is rejected outright by any AgentOS released before this feature. Writing the key unconditionally meant that upgrading and saving config once — which the setup flow does routinely — left the operator unable to roll back, over a feature they had never used. The section is now written only when a skill setting is actually configured, following the same pruning the file already does for `agents` and unused credential fields. An install that ignores skill config produces a byte-wise unchanged [skills] section. CHANGELOG gains upgrade notes for the rest of the behaviour changes existing installs will see: two .env line forms that were previously parsed into unusable keys now take effect, CLI logs move to stderr, and the skill cache is rebuilt once. * docs(changelog): merge the duplicated Fixed section under Unreleased
…view (use-agent-os#128) * test(memory): add a curated-memory retention benchmark "Memory works poorly" is a felt thing, not a number, so a rebuild has nothing to prove itself against. This drives the real turn path -- build_services -> TurnRunner.run -> nudge review -> the `memory` tool -- against a throwaway workspace and reports capture, recall, and noise. capture and recall are scored separately on purpose. A fact can be written to MEMORY.md and still be unreachable from a later session (injection budget, formatting, a store the prompt never reads), and a file diff alone scores that as a pass. Three measurement traps this had to avoid, each of which produced confident, wrong numbers on the way here: - Services must be built once and held open. The nudge review is a background turn scheduled after the reply is already on the wire, so building and tearing down services per turn kills it with "Storage not connected" while still reporting a healthy capture rate -- measuring only what the agent saved unprompted. - Both curated files ship with seeded template boilerplate that carries no delimiter. It snapshots as one blob and is re-serialized into many delimited entries on the first write, so subtracting entry-by-entry scores every template line as agent-written noise. - Turn capture is on by default: every turn is indexed and the retriever feeds matches back into the prompt, so a fact can be "recalled" having never reached MEMORY.md. `--isolate-curated` disables it, leaving the curated block as the only path a planted fact can travel. Three corpora: facts.jsonl states facts outright, facts_incidental.jsonl leaks them inside task requests, and facts_long_session.jsonl plants anchors then applies budget pressure. `--memory-char-limit` shrinks the budget so consolidation is reached without a hundred-turn run. The timezone fact deliberately plants UTC rather than a regional zone: the planted value has to differ from whatever the host reports, or a fabricated ambient timezone would score as a genuine capture. Reads and writes nothing outside a temp AGENTOS home. Turns cost real provider tokens and the model is stochastic, so a single trial is a smoke test -- use --trials 5+ before drawing conclusions. * test(memory): record the curated-memory baseline Five trials per corpus against bdacd6d, curated-only isolation. Capture 92%/100%, recall 84%/95% -- which contradicts the impression that memory "works poorly" and is the point of writing it down before a rebuild starts. Two findings the raw rates hide: The agent fabricates profile data. In 3 of 5 incidental trials it saved the host machine's timezone as a durable user fact, from a corpus that never mentions one. Worth being precise about the mechanism: the runtime block (agent.py:3795) emits only a bare UTC offset on this host, so the regional zone name in the saved entry was the model's own inference from that offset. The value existed nowhere in the input. Three prompt surfaces also name timezone as a canonical USER.md field, so the model was largely doing as instructed. The failure mode is precision, not retention. The nudge fired zero times across all ten trials. Its counter resets whenever the turn already used the memory tool, and this agent saves unprompted on nearly every turn, so the review path is dormant while the model is diligent -- and largely untested in practice. Recall is understated: three misses were a turn timeout and two provider 404s, which the harness cannot distinguish from a wrong answer. The explicit corpus's 0.2 noise is likewise a matcher artifact -- the agent split a two-part fact across two entries, which is arguably correct. Records conditions, caveats, and what these corpora do not cover, since the felt failure is likely in long sessions or cross-session accumulation rather than anything measured here. Raw run JSON is deliberately not committed: it embeds environment-derived model output, which is how the host timezone leaked in the first place. * test(hygiene): fail when a tracked file carries the host timezone A regional IANA zone locates a contributor as precisely as a home path does, and it arrives the same way: someone writes a fixture from the machine in front of them. That is exactly what happened to the memory benchmark corpus, and the value then propagated into recorded model output before anyone noticed. Banning regional zones outright would be wrong -- the scheduler suite needs real zones to exercise DST and the docs need example values, some 55 legitimate uses in all. What is never legitimate is *this* machine's zone appearing in a tracked file, so that is what the check looks for. It fires on the machine where the mistake is being made and skips in CI, which is the right trade for an authoring guard. Resolving the host zone needs more than the obvious one-liner: on macOS `datetime.now().astimezone().tzinfo` is a fixed-offset object with no `.key`, so the check would report nothing and pass vacuously on the very machines it is meant to protect. `/etc/localtime` is a symlink into the zoneinfo tree on macOS and Linux alike and carries the real name. Also swaps the one pre-existing occurrence, a chat time-prefix fixture, for `UTC` at a zero offset. It only needs to match the strip regex. * fix(memory): stop teaching the agent to save ambient context as user facts Benchmarking curated memory turned up a precision failure: in 3 of 5 trials the agent wrote the host machine's timezone into USER.md from a conversation that never mentioned one. It was doing as instructed. Three prompt surfaces named timezone as a canonical USER.md profile field, and the per-turn runtime block is concatenated into the user's own message before the model sees it (agent.py:3850), so an injected value is indistinguishable from something the user said. Worse, the block emits only a bare UTC offset on a host whose tzinfo carries no IANA key -- the regional zone name in the saved entry was the model's own inference from that offset, a value that existed nowhere in the input. Timezone comes out of all three lists, and the write guidance now says plainly that the runtime environment -- date, time, zone, workspace path, OS, shell -- is injected context and never a fact about the user. Two rules ported from hermes-agent (MIT, (c) 2025 Nous Research) come in alongside, both aimed at what memory is *for* rather than what it may contain: - Declarative, not imperative. "User prefers concise responses", not "Always respond concisely" -- imperative entries are re-read as standing directives in later sessions and can override the request in front of the agent. - The one-week test. A fact that will be stale in seven days is not durable: no PR numbers, commit SHAs, or task progress. Also drops the dead `## Current Date & Time` section. Its only content was `Time zone: {{ timezone }}`, and the sole production caller never passed the argument, so it always rendered `UTC` while the per-turn block reported the real offset -- two contradictory timezones per turn. The per-turn block is authoritative and stays. * fix(memory): let the review fire even when the agent saves unprompted The nudge review fired zero times across ten benchmark trials. The counter was cleared whenever a turn used the memory tool, on the reasoning that an agent curating unprompted needs no nudge -- but a diligent model saves on most turns, so the counter never reached the interval and the review never ran at all. The two are not substitutes. A per-turn save records the one fact that was salient in that turn; the review re-reads the whole conversation and consolidates. Only the second one prunes and merges. So a self-directed write now holds the counter instead of clearing it, mirroring the early-return that excluded run kinds already use. The review is delayed by that turn, not cancelled. The one test pinning the old behaviour is rewritten, and a second test pins the regression directly: saving on every turn must still reach a review. * feat(memory): report what the background review wrote The review writes to memory on the user's behalf with nobody watching, and its event stream was drained and discarded. That has two costs: a memory the user cannot see is one they cannot correct, and a silent review is indistinguishable from one that never ran. The second cost was not hypothetical. The counter bug fixed in the previous commit -- zero reviews across ten benchmark trials -- stayed hidden precisely because a review that never fires and a review that fires and says nothing produce identical output. Memory tool calls are now collected off the review's stream and logged as `memory_nudge.review_done` with a count and a short rendering of each write, which surfaces them in `agentos logs` and the Control UI logs page. Batches collapse to their operation count: naming every op would push a single line past anything readable, and the count already separates a consolidation from a one-off save. Entries are capped at five per line and truncated at 120 chars. The point is to show what landed, not to reproduce the store. * test(memory): record the measured effect of the memory fixes Same corpora, same matcher, re-run after the three changes. Fabrication is gone: the incidental corpus wrote no invented profile entry in any trial, down from 3 of 5, with capture and recall holding at 100%/95%. The review now runs -- seven reviews across five explicit trials, up from zero -- and each reports what it wrote. Records two things the table does not show. The incidental corpus still reports zero reviews, which is the honest limit of holding the counter rather than clearing it: every turn there carries save-worthy content, so the agent saves on all of them and the counter never advances. An agent that saves on literally every turn still gets no consolidation pass, and bounding the hold would close that. The explicit corpus's residual noise is the known matcher artifact, not a regression. * fix(memory): bound how long self-directed writes can defer the review Holding the counter instead of clearing it fixed the common case but left the pathological one: an agent that writes memory on every single turn still never advanced past the interval, so it was still never reviewed. The benchmark shows this is not theoretical -- on a corpus where every turn carries something worth saving, the model saves on all of them. Every user turn now advances the counter, including one that wrote memory. A write still defers the review, because running one immediately after the model curated is mostly wasted, but the deferral is capped at two intervals. Past that the review runs regardless. The two behaviours are not substitutes. A per-turn save records the fact that was salient in that turn; the review re-reads the whole conversation and consolidates, and only the second one prunes and merges. * test(memory): match planted facts at word starts Substring matching scored `go` against "Django" and `no ` against almost any sentence, so several rates were inflated by matches that were not really there. An alternative must now begin at a word boundary but may carry a suffix, so `mock` still matches "mocks" and `friday` matches "Fridays". Both halves are load-bearing: anchoring the trailing end too stopped a correctly-captured "never suggest mocks in tests" from matching `mock` at all, which read as a 40-point regression that had not happened. This matters beyond tidiness -- these numbers exist to be compared against a later rewrite, and a baseline wrong in either direction misreads the result. * test(memory): correct the measured effect and pin the matcher Re-measured with the corrected matcher, and the retention improvement reported earlier does not survive. Capture and recall are unchanged -- 92%/84% explicit, 100%/90% incidental -- and the 92% -> 96% / 84% -> 92% seen in an earlier pass was run-to-run variance on a stochastic model, not an effect. BASELINE.md now says so plainly rather than leaving a flattering number in a table someone will quote. What did move are the two things the fixes aimed at, and neither depends on the matcher: fabrication is gone (no invented profile entry in any trial, down from 3 of 5, and a fabricated entry matches no planted fact under any rule), and the review runs (twelve and ten, up from none, with counts taken straight off the log stream). Adds a test suite for the matcher itself, which was wrong twice in opposite directions -- substring matching scored `go` against "Django", then anchoring both ends stopped `mock` matching "mocks" and read as a 40-point regression that had not happened. It decides whether a planted fact counts as reaching memory, so a bug in it moves every rate the benchmark reports, and both failures are now pinned.
…available actions (use-agent-os#129) * feat(env): look for a credential before asking someone to produce one A variable reported as missing often is not. The operator has usually already run `gh auth login`, and telling them to go find a token they effectively lost is worse than looking where it lives. credential_sources registers the places a credential may already exist and answers two separate questions about each. Probing asks "could this supply it" using the source's own status check — `gh auth status`, never `gh auth token` — so a listing can offer an import without any call touching a secret. Reading happens only from an explicit import, because "AgentOS took my GitHub token and handed it to an agent" is not a surprise anyone should get. A test asserts the module exposes nothing that could hydrate on its own. Probe results are cached for a minute: a listing asks about every unset variable it knows, which would otherwise be one subprocess per row per refresh. env.list reports availableFrom on unset variables, `agentos env import NAME` and a "Use GitHub CLI" button act on it, and all three say the same thing afterwards — the value is a copy and will not follow the source's own rotation. Better stated once at import than discovered as a mystery 401. Also fixes a defect from use-agent-os#127: env_key is not always a variable name. Providers that authenticate by OAuth carry the literal string "OAuth" there, meaning "no API key involved", and taking it at face value put a variable called OAuth on the Environment screen that nobody could ever set. Entries now have to look like environment variables — upper case — which also holds for any future sentinel without this needing to know its name. * feat(skills): tell the agent what to do about a missing requirement, per surface skill_view now appends a setup note when a skill's requirements are unmet, and what it says depends on who is listening. A chat channel is told the secret must not be collected there, because a value typed into Telegram is stored in the conversation. An unattended run is told nobody is available and to continue with what works while stating what does not. An interactive session gets the actual command. Getting this wrong is not cosmetic: an agent handed "ask the user for the value" on a messaging surface will faithfully do it, and the credential ends up in a chat log. The skill still loads in every case. An agent that knows what is missing can do the parts that work and say plainly which parts cannot, which is more useful than refusing to open it. When the credential is already reachable — GITHUB_TOKEN with the GitHub CLI authenticated — the note leads with that instead of asking for a value. Ineligibility the operator cannot act on from here, like an OS mismatch, produces no instruction at all; inventing one would be worse than silence. skill_list drops the `Fix: env_set(...)` line shipped in use-agent-os#127. env_set is hidden by default, so that line pointed the model at a tool it usually could not call — the same dead-end this feature exists to remove. The listing keeps the diagnosis; how to fix it belongs in skill_view, where the agent is actually trying to use the skill, not repeated once per entry across a listing of fifty. * docs(web-ui): document the credential import offer on the Environment screen
…s#132) * feat(skills): make publisher real data behind a server-side allowlist Skill cards showed partner branding from a name heuristic, so who stands behind a skill was never actually recorded. Add a SkillPublisher field that a SKILL.md may select by id, and resolve that id against an allowlist so a third-party manifest cannot claim a partner's name, link, or logo — the frontmatter picks a publisher, it never describes one. The snapshot cache gets the same check, and its schema version is bumped so a stale v9 file is rejected instead of silently restoring every skill unbranded. Also drops _LAYER_ORDER, which nothing read; the precedence it documented is the iteration order of _get_layer_dirs(), where the comment now lives. * fix(skills): let the model actually see the skill list The injected skills block spent most of its budget on a <location> line nothing reads — skill_view resolves by name, and its not-found text tells the model not to go looking on disk. It also put the operator's home directory in every system prompt. Removing it drops full mode from 20953 to 16375 chars and compact mode from 6661 to 2083. Full mode still did not fit the 8000-char default, so every default install fell through to name-only: the model has never seen a single skill description. Raise the default to 24000 so descriptions fit with room for installed skills. Truncation kept a prefix of a bundled-first list, so a cut always landed on the skills the operator installed on purpose. Sort by layer precedence first (stable, so within-layer order is untouched) and return the dropped names, which the pipeline step now logs and republishes; skill_count and filtered_skill_ids describe what reached the prompt instead of what was thrown away. Eligibility was built once at import and caches negative which()/env lookups forever, so installing a binary never took effect until restart while the Skills page — which rebuilds per call — reported it ready. Build it per turn instead. * feat(skills): make skill availability an answerable question Nothing could say whether a skill was actually being offered to the agent, or why not: the gate lived inside the engine step, ran only during a turn, and recorded its verdict solely by a skill's presence in or absence from the injected prompt. Move the gate and the budget decision into agentos.skills.availability as gate_skills / plan_injection — pure functions over their inputs — and have filter_skills call them. Both now yield a SkillAvailability per skill (offered, reason, human-readable detail), published as ctx.metadata["skill_availability"], so the same two functions answer the question for a turn and for a caller asking before any turn has run. Ineligibility routes through diagnose_eligibility, so the detail names the missing binary, the missing variable and what it is for, and the install command, rather than a bare "ineligible". Details never carry a filesystem path. Also swaps the /home/... stand-in in test_skills_injector.py for /opt/..., which the public-release-hygiene path check reads as a user home. * feat(skills): make how a skill was acquired a first-class fact The Installed tab grouped skills by loader layer, which answers "which directory did this come from" — not the question the UI actually asks, which is "did I install this, and can I remove it". The lockfile knew the real answer and only the Community "Installed" chip ever read it. SkillAcquisition (shipped | hub | local) derives that from the lockfile per request. It is deliberately not a SkillSpec field and not written to the skill snapshot: it changes without any SKILL.md mtime changing, so caching it would go stale in a way the manifest check cannot detect. build_skill_inventory() is the one row builder — spec, eligibility, acquisition, publisher, availability — so the surfaces that each rolled their own answer can converge on the same facts. Two guards, because an Uninstall button that half-succeeds is worse than none: the lockfile path is resolved from the state root while the managed dir is config-overridable, so when they disagree the row reports hub with removable=False and says why; likewise for an entry whose directory was removed by hand. Update stays available in both cases — it re-fetches by identifier into the current managed dir. Hub installs now carry their publisher: a fetched skill has no publisher: block of its own, so the catalog row's provider is recorded in the lockfile and resolved through the same server-side allowlist a manifest goes through. A hub cannot mint brand identity either — an unrecognized provider is kept for diagnostics and renders unbranded. * feat(skills): give every surface one answer about a skill The CLI, the Web UI, and the agent each assembled a skill row by hand and only one of them read the lockfile, so the same install described itself three different ways. skills.list, skills.status, skills.get, `agentos skills list` and the agent's skill_list now all render from build_skill_inventory, and the payload gains publisher, acquisition and availability alongside every key it already had. Browse also stops hiding installs. A catalog is free not to list something the user installed — a GitHub install by URL is in no catalog at all — so skills.search unions the router results with rows synthesized from the lockfile, deduped by identifier and appended after the catalog rows. The installed chip now joins names to names and identifiers to identifiers instead of pooling both into one set, which flagged a catalog row whose name happened to equal an unrelated skill's identifier. * refactor(skills-ui): group the Installed tab on publisher and provenance The Installed tab grouped on `layer`, which is a location — which directory a SKILL.md was loaded from — so one partner's skills split across two headings the moment one of them was installed from a hub. Group on the data instead: Partners (any allowlisted `publisher.id`), then shipped / hub / local from `acquisition.kind`. A skill lands in exactly one group and Partners wins, so Bankr and Robinhood sit under one heading. Delete `isRobinhoodSkill` / `robinhoodSkills`. They inferred a brand from the skill's own name and homepage and leaned on `layer === 'bundled'` to stop a community skill wearing the banner. That guard now lives server-side, where the declared publisher is resolved against an allowlist before it reaches the wire, so the client trusts `publisher.id` and reads neither name nor homepage. Add the availability derivations: a skill can be installed, eligible and still never offered to the agent. An absent block means "not computed" — the CLI never computes one — and must not render as not-offered. `filterRegistry` re-filtered the server's own search results over a narrower matcher than the server used, dropping legitimate hits; the server also matches tags, which are not on the wire, so no client matcher can reproduce it. Pass `serverFiltered` once the rows on screen are the server's answer and only the category chip narrows them. The text pass still runs (now including category) while a request is in flight, where narrowing a stale list is harmless. `mergeRegistryRows` unions two registry lists by key with base winning, so a just-installed row shows on the same tick instead of after the refetch. SkillsPage.tsx gets only the mechanical substitutions needed to keep the build green; its Partners rendering is the next change. Its fixtures now carry the publisher and acquisition blocks the gateway sends, and the layer-heading assertion is updated because it pinned exactly the grouping this changes. * feat(skills-ui): make the Skills page tell one story The Installed tab now groups on provenance instead of the loading layer, so Bankr and Robinhood sit under one Partners heading whether a skill shipped with AgentOS or came from that partner's hub. The layer moves to a per-card detail chip and stays in the dialog, so precedence is still debuggable. Update and Remove come off `acquisition.removable` / `.updatable` rather than `layer === 'managed'`. A custom `skills.managed_dir` used to render an Uninstall button that deletes the lockfile entry and leaves the files; that skill now explains why there is no button instead. A hand-copied directory inside the managed dir loses both buttons, which is what it always deserved. The partner card's hardcoded `bundled` source label becomes the row's real acquisition, since a partner hub install reaches the same tab. Availability is the page's third state: installed, eligible, and still not offered to the agent. The card carries the reason as text next to the dot, and the dialog spells out the gateway's explanation — a user who installed twelve skills can now see why the twelfth is not reaching chat. The Community list stops swapping between the browse snapshot and the live query: it merges, so a row installed while searching survives the search being cleared even before the refetch lands. Three defects in the same code path go with it — the open dialog no longer unmounts when the list underneath it changes, uninstall invalidates the catalog so the Installed chip is not stale, and a failed live search renders as an error rather than "no results". Client text filtering is skipped once the rows on screen are the server's own answer, which the server matches on tags the client never receives. Group headings become real h2s. The test mock returned the same `skills.search` result for every call, which is why the swapping list had no failing test; it is query- and source-aware now. * docs(skills): document publisher, acquisition, and availability The Skills surfaces now report four separate facts about a skill - where its files are, how it was acquired, whose name is on it, and whether the agent is actually being offered it - and the docs described only the first. - src/agentos/skills/bundled/agentos/SKILL.md: layer is no longer THE organizing concept; add acquisition, the allowlisted publisher, and the availability.reason table, and rewrite the "skill missing from prompt" troubleshooting so a budget drop is diagnosable at all. - docs/cli.md: the new publisher/acquisition keys on skills list --json, and why availability is deliberately absent there. - docs/features/skills.md: a section separating the four facts, and a troubleshooting flow keyed on the reason the surface reports. - docs/web-ui.md: the Installed tab's new headings, ready-vs-offered, and the community browse behaviour. - docs/configuration.md: max_skills_prompt_chars, now defaulting to 24000. - CHANGELOG.md: merged into the existing Unreleased headings, with the two user-visible changes called out under a new Changed section. The env feature's promise that setting a variable applies without a restart is now true for the chat path as well; web-ui.md says so explicitly instead of leaving the agent's own view unstated. THIRD_PARTY_NOTICES.md is untouched - the two Robinhood manifests gained a publisher block, not a provenance change. * fix(skills): restrict who may claim a partner's name The publisher allowlist stopped a manifest from *describing* a brand, but not from *selecting* one: any directory dropped into a writable skills path could write `publisher: {id: robinhood}` and land in the Partners group with Robinhood's name and link. Allowlisting the fields only moved the forgery from the fields to the id. So a manifest may now select a publisher for itself only when it ships inside the wheel. Everything reachable from a writable skills path — managed, personal, project, workspace, extra — is operator- or hub-supplied text and gets no brand from its own frontmatter; an installed partner skill such as Bankr is branded by the hub catalog row that installed it, carried in the lockfile, so it keeps its heading. The snapshot cache is re-resolved through the same gate, which also corrects a cache written before it. Also collapse the three disagreeing prompt-budget defaults onto one constant: the turn pipeline still fell back to 8000 when a turn arrived without a skills config, which is the exact value that forced default installs into name-only mode. Two assertions changed because they pinned the behaviour this reverses: `test_a_declared_publisher_wins_over_the_lockfile` asserted a managed skill could override its hub's brand, and the third-party impersonation test placed its skill in the bundled layer, where the guard does not apply. * fix(skills): keep a partner's brand on installs that predate publisher ids Every lockfile written before publisher_id existed carries an empty one, so a Bankr skill already installed on the previous release resolved to no publisher and fell out of the Partners group into "Installed from a hub" — the exact split heading issue use-agent-os#130 set out to remove, on exactly the machines that filed it. _derive_publisher now falls back to entry.source, the same selector install time already falls back to, so an old entry resolves to the same allowlisted record a fresh one does. An unrecognized source still grants no brand. Also: the installer never wrote LockEntry.version, which was invisible until acquisition.version went on the wire; and the truncation docs claimed bundled skills are cut before anything an operator installed, when the cut follows layer precedence and takes `extra` dirs first. * fix(skills): stop the Bankr source installing skills it does not publish A hub install records its publisher from the source it came through, so BankrSource.fetch/inspect delegating to GitHubSource unchecked meant skills.install(identifier="<any github url>", source="bankr") installed arbitrary code and recorded it as published by Bankr — rendering it under Bankr's name and link in the Partners group. Gate both delegating methods on the allowlist this source already declares, so the boundary applies to installs and not only to catalog listings. * fix(skills): answer the budget question without a turn, and stop three lies Four follow-ups from the review of the source-of-truth change: - The prompt-budget verdict was computed only inside a turn, so a row could never carry `prompt_budget` and the Skills page could not deliver the half of the promise that says "or gives a specific reason". The budget gate is not turn-specific — it depends on the gated set and the configured budget, both known when a row is built — so the inventory runs it too. Retrieval stays out: it ranks against one message's wording. Docs corrected to say which reasons a row can carry. - With `[tools] enabled = false` the Skills page answered against the full process registry while chat answered against a turn with no tools, so a tool-gated skill read as available and was then withheld. - A stale lockfile entry whose name collided with a shipped skill made that skill render as hub-acquired, with a source label, a partner's brand, and a Remove button that could not apply. Nothing can install into the packaged bundled directory, so such an entry belongs to a different install. - Update/Remove read `acquisition?.updatable === true`, which is false when the key is absent, so a stale frontend against an older gateway lost both buttons instead of falling back to the layer gate they used before. * fix(skills): stop the skills block arguing against its own contents The guidance above <available_skills> opened with "Skills are optional task playbooks" and told the model to load one "only when a listed entry clearly matches the user's current request". In compact mode — which a stock install always fell into, because full mode never fit the budget — that same block listed nothing but names. A bare name matches nothing clearly, so the instruction was unfollowable and the honest reading of it was "skip it". The two failure modes are not symmetric. Loading a skill that turned out to be unnecessary costs a little context; skipping one that carried the right endpoints, commands, or conventions produces a confidently wrong answer, and a skill also encodes how the user wants the task done here, which is not inferable from the request. Say that, ask for a load on partial relevance, and in compact mode state plainly that a name alone cannot rule a skill out. Full mode grows 16375 -> 16589 chars against a 24000 budget, leaving room for roughly thirteen more skills before compact mode is reached. The truncation test derived its budget from a magic number, so editing this prose changed which case it exercised; it now derives the budget from a real render. A sibling test could pass vacuously once truncation stopped firing, so it now asserts something was dropped. * fix(skills): notice skills that other agents add and remove The skill directories are not exclusively ours. `agentos skills install` runs in its own process, and `~/.agents/skills` is a cross-agent convention that Codex, Cursor, and others write into while an AgentOS gateway is already up. Two separate assumptions meant neither reached a running gateway: - `load_all()` returned its in-memory cache unconditionally, and the cache is only cleared by AgentOS's own install/update/remove paths. A skill written by anyone else sat on disk, absent from `skills.list`, absent from the prompt, with nothing logged. It is now validated against the same file manifest the on-disk snapshot already compares — one stat sweep, measured at 0.6 ms for 65 skills, against a `load_all()` that every caller invokes once per operation. - `~/.agents/skills` and `<workspace>/.agents/skills` resolved once at boot and collapsed to "no such layer" when absent, so the first cross-agent install on a machine stayed invisible until a restart. The managed directory was already exempt for exactly this reason; these two now match it. Naming a directory that does not exist costs nothing — `load_all()` and `_build_manifest()` both skip a missing path. Each new test fails against the previous implementation. * fix(config): lift an existing skills budget of 8000 on upgrade max_skills_prompt_chars is materialised into every saved config.toml, so raising the default would have reached new installs only — and 8000 is exactly the value that cannot fit the shipped skills' descriptions, which is why it was raised. Existing installs would have stayed on a name-only skill list with nothing telling them why, or that a number they never chose was the cause. This joins the config migrations that already run on the next gateway start and take a timestamped backup, so an upgrade is enough; nobody has to know to edit the file. Only the exact old default is rewritten, the same rule the legacy model ids use — a budget someone picked deliberately is left alone, and an unset key is not materialised. One test asserts the point rather than the number: the value it lifts to has to actually fit the shipped set in full mode, or the migration is cosmetic.
The Ollama plain-text example hardcoded model = "qwen2.5:7b" and agent_max_iterations = 8. Copying the snippet could overwrite a user's configured model or point at a model that is not installed locally, and the tool-loop cap is irrelevant while tools are disabled. Model selection is now a separate step (ollama list + agentos configure provider), and the TOML block shows only the settings plain-text mode requires. Co-authored-by: Preciousuche <85787225+Preciousuche@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…intext-model-selection docs(config): separate model selection from plain-text mode settings
Cut the 2026.7.27 release across every touchpoint the release consistency and install-script guards check: pyproject.toml, uv.lock, CHANGELOG.md ([Unreleased] moved into a [2026.7.27] section with an empty [Unreleased] reopened), RELEASES.md, README.md install examples and wheel URL, install.sh / install.ps1 defaults and usage text, and the CURRENT_VERSION / CURRENT_RELEASE_TAG constants in tests. No runtime code changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….7.27 chore(release): bump version to 2026.7.27
# Conflicts: # frontend/src/views/skills/SkillsPage.tsx
Standardizes the route title, sidebar item, page heading, browser tab title, and docs on `Agent Setup`. Closes use-agent-os#123
…-os#131) Cmd/Ctrl+Shift+O starts a new chat from anywhere in the console, reusing the same startNewChat flow as the button. The tooltip shows the platform-appropriate hint. Closes use-agent-os#120
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….7.28 chore(release): bump version to 2026.7.28
The three CI jobs that need the bundled ONNX weights each checked out with `lfs: true`, so every run pulled ~45 MB (bge + MiniLM + pilot) three times. That exhausted the account's Git LFS bandwidth quota and blocked checkout on every job with "This repository exceeded its LFS budget". All three genuinely need the real weights, so the fix is not to drop LFS: `test_pilot_encoder.py` loads the MiniLM export through ONNX Runtime, and `test_build_wheelhouse_zip.py` asserts the built wheel carries a hydrated multi-megabyte ONNX rather than a 130-byte pointer. Instead the jobs now check out without LFS, restore `.git/lfs/objects` from the Actions cache (free, and not billed as LFS bandwidth), and only fall back to a network `git lfs pull` on a cache miss. The cache key is the sha256 of the sorted LFS oids, so it invalidates exactly when the weights change. A hydration check runs after the restore. Pointer files still satisfy the `.is_file()` guards in the pilot encoder tests, so an incomplete restore would not skip those tests — it would fail later with an opaque ONNX parse error. Failing fast at the checkout step keeps that diagnosable. The release workflows keep the direct `lfs: true` + `git lfs pull` path; their hydration asserts are the last line of defense against shipping a pointer inside a published wheel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s-objects ci: restore Git LFS objects from the Actions cache
Bankr publishes in two places. The skills in BankrBot/skills are what the Bankr source has been able to read; anyone can also publish from bankr.bot, where a skill lives under its author's wallet address and is served by api.bankr.bot/public/skills/<wallet>/<slug> as JSON with the body inline. There is no repository to clone and no catalog.json, so those skills were invisible to browse and un-installable — adding the slug to the existing allowlist just 404s. Carry them on a second allowlist with their own load path: the SKILL.md is synthesized from the payload (frontmatter from the JSON fields, body verbatim; a body that already ships frontmatter is left alone) and the catalog load fans out to both halves on one client. The request is deliberately unauthenticated — the GitHub token the repo half carries has no business being sent to another host — and the payload is bounded, because it is community-controlled text. The install path is gated the same way as the repository half: bankr.bot serves every published skill from one host, so an ungated source would let `skills.install(source="bankr")` pull any author's skill and record it as having come through Bankr's hub. A wallet-published skill is credited to its author and resolves to no recognized publisher, so it renders unbranded rather than in the Partners group, and author avatars are dropped rather than widening the console's img-src CSP. Ships stock-premium-lp-manager as the first such skill. Fixes use-agent-os#149
# Conflicts: # CHANGELOG.md
…ills feat(skills): install Bankr skills published from bankr.bot
An external contributor opened a PR adding a point-in-time SECURITY_AUDIT.md at the repository root. The existing policy covers where to send vulnerability details but says nothing about audit documents, reward programs, or how researchers get credited, so there was nothing to point at when declining it. Add an "Audit Reports and Scanner Output" section covering all three: route findings through the private advisory form rather than a PR, no bug bounty exists or is planned, and researchers whose reports lead to a fix are credited in that fix's release notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…udit-report-policy docs(security): reject audit reports as PRs and state no bounty program
Two changes land since v2026.7.28: Bankr skills published from bankr.bot under an author's wallet address are now browsable and installable through the Bankr source (use-agent-os#150), and SECURITY.md states where audit reports belong, that there is no bug bounty program, and how researchers are credited (use-agent-os#154). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….7.29 chore(release): bump version to 2026.7.29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prevent Web UI skill cards from ignoring their CSS grid track and overlapping adjacent cards when skill names are long.
min-width: 0to.sk-cardand.sk-card__headflex/grid containers inskills.css.min-width: 0,overflow: hidden,text-overflow: ellipsis, andwhite-space: nowrapon.sk-card__name.skills-css.test.ts.Closes use-agent-os#135