Skip to content

fix(webui): prevent skill cards from overflowing grid tracks - #1

Open
Preciousuche wants to merge 94 commits into
mainfrom
feat/capminal-skills
Open

fix(webui): prevent skill cards from overflowing grid tracks#1
Preciousuche wants to merge 94 commits into
mainfrom
feat/capminal-skills

Conversation

@Preciousuche

Copy link
Copy Markdown
Owner

Summary

Prevent Web UI skill cards from ignoring their CSS grid track and overlapping adjacent cards when skill names are long.

  • Added min-width: 0 to .sk-card and .sk-card__head flex/grid containers in skills.css.
  • Enforced min-width: 0, overflow: hidden, text-overflow: ellipsis, and white-space: nowrap on .sk-card__name.
  • Added contract regression coverage in skills-css.test.ts.

Closes use-agent-os#135

thanhtan1105 and others added 30 commits July 21, 2026 22:23
…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
…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
keyKQ and others added 30 commits July 27, 2026 15:30
…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
…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
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.

webui(skills): skill cards ignore their grid track and overlap the next card

7 participants