Skip to content

Release/1.8.0#694

Merged
philmerrell merged 1706 commits into
mainfrom
release/1.8.0
Jul 19, 2026
Merged

Release/1.8.0#694
philmerrell merged 1706 commits into
mainfrom
release/1.8.0

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

Feature release delivering Skills v2, completing the session-metadata static-sort-key migration, and consolidating the artifact tool-catalog rows.

Full detail: RELEASE_NOTES.md · CHANGELOG.md

⚠️ Operator actions required

Three backfill scripts must be run per environment, each dry-run first (all three are dry-run by default and idempotent), dev before prod:

Script Required? Notes
backfill_artifact_tool_merge.py Yes Promotes grants before the retired update_artifact row is deleted. Skipping it drops the tool for roles granted update_artifact alone.
backfill_skill_bundles.py Yes, if the env has pre-v2 skills Copies legacy objects; only removes them with explicit --delete-legacy.
backfill_session_static_sk.py Optional Unlocks the Phase 3 single-query read path. Deletes are permanent, no rollback. Listing works correctly either way.

Skills go live the moment backend.yml completes — before any CDK deploy — because deployed containers set no SKILLS_ENABLED and unset now reads as enabled. To hold an environment back, set CDK_SKILLS_ENABLED=false and deploy platform.yml first.

What's in it

Skills v2 (#679#685, #692). Skills were containers that bound tools; they are now pure knowledge bundles — instructions plus reference files — loaded on demand via progressive disclosure (name/description → SKILL.mdread_skill_file). Stored as portable agentskills.io S3 bundles with a SKILL.md write-through projection. The runtime moved to the Strands AgentSkills plugin. Any signed-in user can now author skills at /my-skills, and a skill bound to a shared Agent resolves for whoever uses that Agent.

Session-metadata migration, Phases 2–3 (#677, #678). A cold-tail backfill for rows that never get written again, plus a read contraction that skips the legacy query once the backfill's marker is set. Fails open to dual-read, so the deploy is order-independent.

Artifacts consolidated (#689). "Create Artifact" and "Update Artifact" become one Artifacts toggle that injects both tools.

Breaking

  • Skills no longer bind tools. Conceptual, not data: existing rows keep working and keep their allowedTools, but those values stop granting. Anything relying on a skill to confer tool access must grant via RBAC instead.
  • The update_artifact catalog row is retired — see the backfill above.

Notes

No new AWS resources, no new IAM, no dependency changes. The only CDK delta is the SKILLS_ENABLED env var on app-api and the inference-api Runtime.

The "My Skills" sidenav entry is deliberately hidden pending a navigation decision — the route, page, and backend surface are live and reachable by direct URL.

🤖 Generated with Claude Code

philmerrell and others added 30 commits July 6, 2026 09:12
B1's update_scheduled_prompt skipped None, so a PATCH could never detach a
schedule's assistant or reset its tool restriction — the SPA's clear
checkboxes were inert (a bare null reads as 'leave unchanged').

Add an explicit clear contract: an UNSET sentinel in the service
distinguishes 'omitted' (leave) from None (clear -> REMOVE the attribute).
UpdateScheduleRequest gains clearAssistant/clearTools booleans (rejected if
combined with a value). clearAssistant reverts to the default agent;
clearTools re-snapshots the caller's current RBAC-allowed tools, mirroring
creation so a schedule never stores an unresolved None. SPA sends the flags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin GET /admin/bedrock/models endpoint calls the Bedrock control
plane's ListFoundationModels, but the App API Fargate task role only had
bedrock:InvokeModel. In deployed environments the call raised
AccessDeniedException, which the app-wide AWS-error handler maps to a
generic 502 ("Upstream service error."). It only worked locally because
local dev runs with the developer's broader AWS credentials.

Add a BedrockListFoundationModels statement granting
bedrock:ListFoundationModels and bedrock:GetFoundationModel. These are
account-level list/read actions that do not support resource-level
permissions, so they are granted on `*`.

Requires a platform.yml (CDK) deploy to take effect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on-inset-grouping

feat(assistant-editor): group knowledge base into a contrasted inset panel
…ock-list-models-iam

fix(app-api): grant bedrock:ListFoundationModels to task role
…d-runs-pr5a-hardening

feat(schedules): PR-5a hardening — lean-image guard + clearable schedule fields
…26-07-03

chore(kaizen): weekly review prep 2026-07-03
…ged-harness-spike

docs(kaizen): managed-Harness build-vs-adopt spike brief
A scheduled (unattended) run delivers a session the user wasn't watching.
Surface it: the sidebar shows an unread dot until they open it.

- sessions/metadata.py: set_session_unread / mark_session_read — a targeted
  single-attribute UpdateExpression on the row's current SK (GSI-resolved),
  concurrent-safe with the title/activity writes; preview-session guarded;
  best-effort (never raises).
- harness/runner.py: mark the delivered session unread only on a *completed*
  run with trigger == "schedule" — attended "Run now" and failed/consent-
  blocked runs never set the dot (the user is present / there's nothing to
  read).
- app_api POST /sessions/{id}/read: clears the durable flag when the user
  opens the session; idempotent, ownership-enforced via the GSI lookup.
- SPA: durable server-persisted unread on SessionMetadata (survives reload,
  reaches other devices) ORed with ChatStateService's ephemeral in-tab
  unread for interactive background completions; session-list renders the dot.

Backend 108 + frontend 25 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-unread-scheduled-runs

feat(sessions): unread indicator for scheduled-run deliveries
Phil-initiated kaizen focus: scope a future bedrock-runtime (Converse) ->
bedrock-mantle endpoint migration. Watchlist/Defer — strategic alignment
with where Bedrock capability lands first, not near-term need; Claude-on-
Mantle is Messages-API-only today and lacks cross-region + native
CountTokens + Guardrails, so the primary chat path stays on bedrock-runtime.
Interim low-risk value = finishing the already-scaffolded non-Claude
OpenAI-compatible Mantle lane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…le-watchlist

docs(kaizen): Bedrock Mantle endpoint watchlist item
…wered

Complete the #570 build-vs-adopt spike for the headless/scheduled lane.
Answered from the now-GA AWS managed Harness docs cross-checked against
our code and the proven F1 entrypoint spike:

- Q1 RBAC -> allowedTools: qualified yes (per-invoke globs; we already
  snapshot the RBAC-narrowed set statically at the app-api boundary).
  Non-membership gates relocate (quota/cost -> dispatcher; approval ->
  exclude on headless; consent -> Identity outbound).
- Q2 per-user tokens: yes on mechanism (OAuth-inbound customJWTAuthorizer
  == our authorizer; Gateway outbound == our USER_FEDERATION exchange).
  SigV4 cannot do per-user identity. One residual: customParameters
  vault-key pinning through the Gateway-managed exchange -> live probe.
- Q3 lose MCP Apps + SSE on headless: yes (interactive-only affordances;
  SPA loads the delivered session, not the harness stream).

Recommendation: green-light a narrow InvokeHarness probe to close the Q2
residual; interactive inference-api untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hide the "Scheduled Runs" menu option from non-admins while the feature
is still maturing. Adds an isAdmin() check to the existing capability
gate, mirroring the admin-dashboard nav pattern. isAdmin is already
wired into the sidenav component from UserService; showSchedules keeps
its accessibility-probe behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d-runs-system-admin-only

feat(sidenav): gate Scheduled Runs nav entry to system_admin
…ness-spike-findings

docs(kaizen): managed-Harness spike findings — 3 gating questions answered
Records the live dev-ai probe (2026-07-06) that closes the Q2
customParameters residual from the spike findings.

Confirmed live (real CreateHarness/InvokeHarness via boto3): our exact
customJWTAuthorizer is accepted (harness READY); outboundAuth.oauth
customParameters is a first-class field persisted verbatim on GetHarness
(so we CAN pin the same params the consent flow uses); OAuth-inbound runs
as the owner (HTTP 200); the exchange calls the same GetResourceOauth2Token
our get_token_for_user uses; a failed exchange surfaces legibly as a typed
runtimeClientError stream event (maps to paused_reauth).

Boundary found: the managed Gateway 3LO (AUTHORIZATION_CODE) exchange
fails with "must provide a ResourceOauth2ReturnUrl" and does not source
that URL from defaultReturnUrl / OAuth2CallbackUrl header / workload
AllowedResourceOauth2ReturnUrl — a GA wiring gap. Cross-workload token
visibility (platform vs harness-own workload identity) unreached past it.

Decision: GO to adopt Harness on the headless lane, but keep
customParameters-sensitive / all 3LO connectors on our own
get_token_for_user until the return-URL wiring is resolved with AWS.
Aside: managed memory is on by default per harness (relevant to F5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Mark as read / Mark as unread" toggle to the session options menu
in both the top nav and the sidebar session list, backed by a new
POST /sessions/{id}/unread endpoint (mark_session_unread) that mirrors the
existing /read verb. The client surfaces the dot instantly via the
ChatStateService unread signal while the durable server flag lands async.

Fixes two sidebar-only bugs where the in-row options trigger lives inside
the row's group, so the CDK menu restoring focus to it on close tripped
group-focus-within:

- Bug 1: the ellipsis trigger stayed visible after a menu action. Switch
  its reveal from :focus / group-focus-within to :focus-visible, so
  mouse-restored focus no longer reveals it while keyboard nav still does.
- Bug 2: a freshly marked-unread dot didn't appear until the next browser
  interaction. The dot was being hidden by group-focus-within (restored
  trigger focus); scope that to group-has-[button:focus-visible] instead,
  and kick a synchronous refreshSessions() in the mark-unread branch
  (mirroring mark-read) so the OnPush row re-renders immediately.

The top-nav toggle was unaffected because its trigger sits outside the row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…read-unread-toggle

feat(sessions): mark-as-read/unread toggle + sidebar dot fixes
…asts

Two additions to the Scheduled Runs feature:

- Interval cadence: schedules can now run every N minutes/hours in addition
  to daily/weekly. Adds interval_value/interval_unit to the scheduled-prompt
  model and API, a MIN_INTERVAL_MINUTES floor enforced on create/update/
  resume, and interval_to_minutes() feeding compute_next_run_at. The schedule
  form gains the interval option with matching validation.

- "Run now": run a schedule's prompt headlessly on demand from the schedule
  form. RunNowService fires POST /runs/now (existing app-api endpoint) as
  fire-and-forget and reports progress through a new app-wide toast system —
  BackgroundTaskService (signal-backed task list) rendered by the
  BackgroundTaskToastsComponent mounted in app.html. On completion the run
  materializes as a real session; the list is refreshed and the toast offers
  a "View" affordance.

Tests: backend test_schedules_routes / test_scheduled_prompts /
test_harness_runner (100 passed); frontend schedule-form, run-api, run-now,
background-task, and background-task-toasts specs (30 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d-runs-run-now

feat(schedules): interval cadence + "Run now" with background-task toasts
…26-07-03

# Conflicts:
#	docs/kaizen/review-queue.md
…2026-07-03

chore(kaizen): weekly research scan 2026-07-03
…ness-q2-probe-result

docs(kaizen): managed-Harness Q2 live-probe result — GO-with-boundary
…areable

Reframes the per-user markdown memory spec into the Memory Space primitive:
a named, first-class, bindable, templated, and shareable markdown wiki.
Oliver becomes a Chief-of-Staff template + a bound agent rather than a
special-cased feature.

- Adds the three-layer abstraction (Agent / Memory Space / declarative
  binding) and the structural-config vs. semantic-MEMORY.md split.
- Space-keyed storage (SPACE#{id} + membership records + UserSpacesIndex
  GSI) so sharing is expressible from day one.
- Entry types (entity / episodic / fact), Space Templates, and manifest-
  indexed fields for relational/temporal queries ("who owes what").
- Sharing via owner/editor/viewer grants mirroring assistant collab-edit,
  with run-as-user write attribution.
- Data governance is proportionate: same data class as sessions/artifacts
  behind the same Entra JWT + RBAC — identity-based, no content-inspection
  gate. Deletion-purge + inherited encryption are the only real items.
- 8-PR phasing; PR-1 (data layer) is the next buildout phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…paces-spec

docs(memory): Memory Spaces — bindable, templated, shareable second-brain primitive (F5)
…ess findings

PR #576 merged from a state prior to commit 908f7e1, orphaning the
resourceOauth2ReturnUrl parameter-shape + harness-security cross-check
subsection. This re-applies it onto develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rn-url-recovery

docs(kaizen): recover resourceOauth2ReturnUrl section orphaned by #576 merge timing
Add a first-class "download the entire space as a .zip of raw markdown"
capability (index + all entries, structure preserved, metadata.json),
framed as the user-ownership / zero-lock-in property. Promotes the stubbed
export endpoint into §9 with contents, access, streaming mechanics, and an
import-friendly round-trip note; folds the zip export into PR-5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pace-export-zip

docs(memory): full-ownership zip export of a Memory Space
Adds the F5 Memory Space primitive's data layer — no runtime wiring,
gated by MEMORY_SPACES_ENABLED (default off).

Backend (apis/shared/memory/):
- store.py: S3 content-addressed byte store (sibling of the skills store)
- models.py: MemorySpace / MemoryIndex / MemoryEntryRef / SpaceMember
- templates.py: Blank / Chief-of-Staff / Research-Notebook presets
- repository.py: dedicated memory-spaces table CRUD (META/INDEX/MEMBER
  rows, OwnerIndex + MemberIndex GSIs, Decimal handling)
- service.py: permission-gated lifecycle + sharing + entry/index I/O,
  resolve_permission chokepoint (viewer reads, editor writes, owner
  shares/deletes), content-addressed writes with GC-on-replace
- feature_flags.py: memory_spaces_enabled() (default off)
- 47 moto-backed tests; import boundaries clean

Infrastructure:
- MemorySpacesConstruct: S3 bucket + dedicated memory-spaces DynamoDB
  table (OwnerIndex/MemberIndex GSIs), a per-domain table matching the
  project's actual pattern
- Threaded via PlatformComputeRefs to both compute roles (readwrite S3 +
  DynamoDB); env vars S3_MEMORY_SPACES_BUCKET_NAME /
  DYNAMODB_MEMORY_SPACES_TABLE_NAME / MEMORY_SPACES_ENABLED
- config.ts flag default-off; resource-count assertions updated
- tsc + 429 jest tests pass

Spec updated to reflect the dedicated-table + two-GSI decisions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
philmerrell and others added 25 commits July 18, 2026 09:04
Skills authored before PR-2 have neither the SKILL.md write-through
projection nor the standard bundle layout — v1 stored resources
content-addressed (skills/{id}/{sha256}) with no `kind`. Their S3 prefix is
therefore not a valid agentskills.io bundle: it can't be handed to a managed
Harness or exported as-is, which is the whole point of the projection.

The script fixes both per skill: copies each legacy object to its standard
path, rewrites the row's manifest to point there (adding `kind`), and writes
the SKILL.md generated from the row. It imports `generate_skill_md` and the
key helpers from the live write path, so a backfilled bundle is byte-identical
to one the app writes today.

Follows the backfill_session_static_sk conventions: dry-run by default,
idempotent, throttled, scopeable to one skill. Copies are non-destructive —
legacy objects survive unless --delete-legacy, and a manifest entry whose
bytes are missing is left untouched rather than repointed at nothing.

Applied to dev-ai/web_research: manifest now points at
references/extraction_tips.md, SKILL.md written, read path verified at 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backfilling dev's web_research row surfaced that the seeder itself still
emits v1 shapes, so every fresh environment reproduces exactly the state the
backfill just repaired:

- resources landed at the content-addressed key (skills/{id}/{sha256}) with
  no `kind`, instead of skills/{id}/references/{filename}
- no SKILL.md was ever written, so the seeded prefix was not a valid
  agentskills.io bundle and could not be handed to a managed Harness or
  exported as-is

Both fixed. The slug and frontmatter rules are duplicated from
apis/shared/skills/bundle.py rather than imported, because seed.sh runs this
script standalone after infra deploy without the app package on the path —
the existing content-hash logic was duplicated for the same reason.

Tests now pin the standard layout and the projection, plus a guard that the
seed prose never again names the retired v1 meta-tools (skill_executor /
skill_dispatcher). That drift is what left dev's row instructing the model to
call tools deleted in PR-1/PR-2.

Note the seeders are skip-if-exists, so this repairs new environments only;
existing ones need backfill_skill_bundles.py (dev-ai: applied).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2-user-tier

feat(skills): user-authored skills tier (Skills v2 PR-3)
…R-4)

Wires the chat opt-in picker end-to-end and lands the §6 invoke-through
access predicate. Per spec docs/specs/skills-as-agent-primitive.md §8.

The picker's markup shipped in PR-1 but nothing ever sent its selection,
so the plain-chat skills path had never actually run. Turning it on
surfaced three latent bugs:

- Skill resolution was gated on agent_type == "skill", so the picker
  could never have taken effect. Skills are now driven by the selection
  on any turn; agent_type gates nothing ("skill" stays a ChatAgent alias
  only so stale SPA sessions don't 422).
- The binding resolver gated on AppRoleService.can_access_skill, which
  has no ownership clause — an author was blocked on their own authored
  skill when invoking their own Agent — and whose "*" wildcard matched
  any id at all, including another user's private skill. Clauses 1+2 now
  route through resolve_accessible_skill_ids, which expands "*" over the
  catalog only.
- Paused-turn resume and the construction snapshot both keyed skills off
  agent_type == "skill", which would have orphaned the paused agent of
  any plain-chat turn carrying skills. Both now key off the snapshot's
  own enabled_skills.

Invoke-through (D7) is deliberately AGENT-scoped: it lives in the binding
resolver, not in resolve_accessible_skill_ids, because widening the
shared resolver would leak an Agent owner's private skills into every
invoker's plain-chat picker and bindable palette. The owner-match clause
blocks chain-sharing, and a system-owned Agent gets no invoke-through at
all so RBAC stays the sole gate on catalog skills.

D6 default flip: an absent or empty enabled_skills means no skills, on
both the runtime filter and the picker's untouched-preference default.
The two must agree or the UI would show skills as active that the turn
never loads. It also keeps skills free for turns that don't want them —
an absent selection short-circuits before any RBAC or skill-table read.

read_skill_file needed no per-call predicate: its record set IS the
turn's effective skill set, so there is no id the model can name to reach
a skill the invoker cannot use.

The Designer palette union needed no code — /agents/bindable already
delegates to resolve_accessible_skill_ids, which PR-3 widened.

Backend 4714 passed; SPA 1469 passed across 128 spec files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Light/dark Octocat marks alongside the existing google-* connector
logos. Follows the repo's theme convention: -light is the black glyph
(for light backgrounds), -dark the white one.

Nothing references these yet — they're staged for a GitHub connector.
Unrelated to the skills work in this branch; riding along rather than
sitting untracked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2-selection-surfaces

feat(skills): selection surfaces + invoke-through access (skills v2 PR-4)
…2 PR-5)

Flips SKILLS_ENABLED to default-ON with a kill switch and adds the
infrastructure wiring it never had, closing out the Skills v2 epic (spec
docs/specs/skills-as-agent-primitive.md §8).

SKILLS_ENABLED had zero CDK/workflow plumbing, so "enable it per
environment" was not previously expressible. Adds SkillsConfig to
config.ts, threads SKILLS_ENABLED into both app-api and inference-api
(they must stay in step — design-time refuses to bind a skill while the
flag is off, so a mismatch would let an Agent be built with skills the
runtime then blocks), and forwards CDK_SKILLS_ENABLED in platform.yml
with the empty-string-safe ternary an unset GitHub variable requires.

Feature existence and audience are two independent controls. The flag
says the feature exists in an environment; the new `skills` RBAC
capability says who sees the user-facing surfaces. system_admin holds it
implicitly via its "*" tools grant, so the picker and My Skills stay
admin-only during rollout; GA is one grant of `skills` to the `default`
role, no redeploy.

The gate raises 404, not 403. The SPA hides the My Skills nav entry by
riding the list call, so a 404 hides the surface while a 403 surfaces an
error toast — the failure mode that got the scheduled-runs capability
gate reverted in prod. It also deliberately does not gate the runtime:
an Agent shared to an ordinary user must still resolve its bound skills
(invoke-through, §6/D7), and a capability check there would break
exactly that path.

Verified live end to end against a real agentskills.io bundle
(Anthropic's docx) uploaded as a user skill and bound to an Agent:
L1 8,075 -> L2 9,835 (skills tool, SKILL.md body) -> L3 10,737
(read_skill_file on references/LICENSE.txt). Invoke-through confirmed
with a second non-admin account — the grant resolves through the shared
Agent while the same skill stays absent from that user's own picker and
/skills/mine.

The "session auth, not Bearer" assertion now walks the transitive
dependency tree rather than each route's direct dependencies, since the
routes hang off the capability gate which in turn depends on the
session. Pins the invariant that actually matters instead of the shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2-enable-and-dogfood

feat(skills): enable Skills v2 + admin-only capability gate (skills v2 PR-5)
Skills v2 moved skill authorization to apis/shared/skills/access.py
(resolve_accessible_skill_ids = catalog ∪ own, and resolve_invocable_skill_ids
which adds the Agent-owner invoke-through clause). AppRoleService.can_access_skill
has had zero production callers since PR-4 and is wrong on two axes for anything
user-tier: no ownership clause, and its "*" wildcard matches ANY skill id
including another user's private authored skill.

- Delete the method and its three tests. test_can_access_skill_with_wildcard
  asserted can_access_skill(user, "any_skill") is True — it enshrined the
  wildcard over-expansion bug as expected behavior.
- Fix two stale docstring/comment references in agent_designer's
  binding_validation.py that still named it as the live run-time mechanism;
  since PR-4 that is resolve_invocable_skill_ids.
- Reword the intentional "deliberately NOT can_access_skill" rationale in
  skills/access.py and agent_binding_resolver.py to past tense so they no
  longer imply the function still exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Skills v2 moved skill authorization to
apis/shared/skills/access.py (resolve_accessible_skill_ids /
resolve_invocable_skill_ids). SkillAccessService was left behind with
zero live callers — not exported from admin/services/__init__.py, not
DI-wired, and reached by no dynamic import. Its can_access_skill carried
the same "*"-wildcard over-expansion flaw that got AppRoleService.can_access_skill
deleted in #686.

Also updates the stale comment in admin/skills/routes.py that named the
service as the consumer of the all-skill-ids snapshot; that snapshot is
still live, but its reader is now skills.access.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ac-can-access-skill

refactor(rbac): remove dead AppRoleService.can_access_skill
…ad-skill-access-service

refactor(skills): delete dead SkillAccessService
A browse-and-discover surface for published Agents: a directory page and a
detail page modeled on the ChatGPT/Claude plugin-detail layout, built as a
read-view over the Agent record rather than a new primitive.

The central decision (D1) is that we do NOT introduce a "Plugin" noun. Both
vendors need a bundle layer because their capabilities install into a
workspace separately from any persona; our Agent's `bindings[]` already IS
that bundle, attached to the persona. Every field a vendor plugin-detail page
renders already exists on our record.

Grounding findings that shaped the design:

- `VisibilityStatusIndex` (GSI2) is live and populated, and PUBLIC *access*
  still resolves to "viewer" — only the *listing* was switched off in
  ad4437e when email sharing superseded a public index. The read path is
  mostly built.
- Listing is nonetheless a new sparse GSI5, not a re-enable: `VISIBILITY#PUBLIC`
  is one hot partition and can't be filtered by category. GSI5 is the next free
  slot (GSI4 is DueSyncIndex), and DueSyncIndex on the same table is the
  precedent — unlisted agents have no key, so the query physically can't see them.
- `listed` is deliberately separate from `visibility` (D3). Deriving listing
  from PUBLIC would retroactively publish every existing PUBLIC agent to the
  whole institution with no author consent; backfill is listed=false.
- Publishing amplifies Skills v2 invoke-through from a typed email list to
  everyone, so the publish dialog must enumerate which authored skills it
  exposes, and memory_space bindings block publication outright (D5).
- The Designer's block-on-missing rule (D5) strains under open browsing, so the
  detail page previews per-invoker runnability up front — which resolves the
  "per-invoker capability preview" open question parked in agent-designer.md.

Also notes a behavior change needing a call: GET /agents/{id} currently returns
`instructions` to any PUBLIC viewer, which is a much larger exposure once agents
are broadly listed than it was under link-sharing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rectory-spec

docs(agents): draft the Agent Directory spec
… toggle

Artifacts shipped as two independent `protocol: local` catalog rows —
`create_artifact` and `update_artifact` — so the tool picker listed them
as two unrelated entries. The picker only groups children under a parent
for MCP protocols (driven by `serverTools`), so there was nothing to nest
them under; the flat listing was a data-model fact, not a template gap.

Adopt the Word-documents idiom already used one entry below in the seed
list: a single catalog row whose id is the gate key, with the runtime
injecting the full toolset. `create_artifact` is now that key and
provisions both the create and update tools.

`seed_default_tools` is create-only, so a seed run does nothing to an
environment seeded before this change. Add a backfill script that
retitles the surviving row, promotes `update_artifact` to
`create_artifact` everywhere a grant can hide — role TOOL_GRANT# items
*and* the grantedTools/effectivePermissions.tools arrays on DEFINITION,
user toolPreferences, assistant bindings — then deletes the retired row.
Promote-before-delete, so an aborted run degrades to "both granted",
never "neither". Schedule snapshots are left alone: a stale id there is
an inert no-op since the runtime only reads `create_artifact`.

Two judgement calls worth recording:

- User prefs are a sparse override map, so only an explicit *enable* of
  the retired id carries over. Someone who switched update off while
  leaving create at its default-on never asked to lose artifacts, so an
  explicit disable just drops the key.
- A role granting `*` gains no concrete grant — the wildcard already
  covers the keeper and narrowing it would be a silent scope change.

Behavior change: anyone with create enabled but update disabled now
gains update. That is inherent to collapsing the toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tifact-catalog-rows

refactor(artifacts): collapse the two artifact catalog rows into one Artifacts toggle
The /my-skills route, page, service, and the whole /skills/mine backend
surface stay fully functional — only the sidenav link is removed, until
we decide how users should actually navigate to their skills.

Drops the now-dead showMySkills computed, the MySkillService injection,
and its loadSkills() accessibility probe (that call existed solely to
decide whether to render the link).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopts the authoring half of Anthropic's open-source skill-creator and
scopes out the eval half, which assumes a filesystem, subagent spawning,
and script execution — none of which exist here (scripts are inert by
design). Evals hang off F1's headless lane instead.

PR-1 ships the methodology as an admin-catalog skill with zero code,
routing the handoff through the My Skills form's existing frontmatter
parsing. PR-2..PR-4 add the missing primitive: agent tools that write
the user's own skills, executing as the invoking user and re-checking
ownership per call, mirroring memory_write.

Depends on skill-bundle-import's nested paths and server-side SKILL.md
parser rather than restating them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…skills-nav

chore(sidenav): hide the My Skills nav entry
…eator-spec

docs(skills): draft the Skill Creator spec
The user-facing skills surfaces (`GET /skills/`, `PUT /skills/preferences`,
and all of `/skills/mine/*`) hung off `require_skills_capability`, which
404'd anyone not holding the `skills` RBAC capability. Its job was to keep
skills admin-only during the v2 rollout, with GA framed as "one grant of
`skills` to the `default` role, no redeploy."

That GA path does not work, for two independent reasons:

1. `default` is a *fallback* role. `resolve_user_permissions` consults it
   only when a user matches zero AppRoles (service.py "Step 3"); it is not
   merged alongside a matched role. Prod's `default` carries no JWT mappings
   at all, so granting there would reach only unmapped users — never the
   faculty/staff/student cohorts.
2. A capability id cannot be granted from the admin roles UI regardless.
   That form builds `grantedTools` from the tool catalog, and a capability
   is not a tool, so there is no way to select it and no free-text entry.

Net effect: an admin who granted a catalog skill to a role would find it
silently invisible to that role's users, with no in-product way to fix it.

Remove the gate. Skills are governed by `SKILLS_ENABLED` per environment and
by a role's `grantedSkills` per cohort — a complete model that the admin UI
can actually operate. Both surfaces are already self-limiting: `GET /skills/`
returns only what `resolve_accessible_skill_ids` grants (no grants means an
empty list and no rendered picker), and every `/skills/mine/*` route is
owner-scoped inside `UserSkillService`.

The route-coverage control is kept rather than dropped, retargeted from
"every route is capability-gated" to "every route requires a session" — the
invariant that still matters now that the session dependency is the only
thing between these routes and an anonymous caller. A second test pins the
removal so the gate cannot creep back without also making capabilities
grantable from the roles UI.

Also corrects the "GA = grant to `default`" claim where it appeared in
capabilities.py, infrastructure/lib/config.ts, and platform.yml, and notes
that `SCHEDULED_RUNS_CAPABILITY` is itself unused (that gate was dropped
after 403ing in prod), which leaves capabilities.py with no consumers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rop-capability-gate

feat(skills): drop the `skills` capability gate from user-facing routes
`apis/shared/rbac/capabilities.py` has no consumers left. It defined two
capability ids granted through the `grantedTools` axis:

* `SCHEDULED_RUNS_CAPABILITY` was orphaned when the RBAC gate on `/schedules`
  and `/runs` was dropped after 403ing in prod.
* `SKILLS_CAPABILITY` was the last live caller, removed in the preceding
  commit along with `require_skills_capability` and its 12 route deps.

Delete it rather than keeping it as a reference. The mechanism it documented
is not one we want reached for again: a capability id cannot be granted from
the admin roles UI (that form builds `grantedTools` from the tool catalog and
offers no free-text entry), so any gate built on it is operable only by
hand-writing DynamoDB items. Both live gates were removed for that reason.

Its docstring carried two findings that cost real effort to establish, so
they move to `AppRoleService.resolve_user_permissions` — the code they
actually describe — rather than dying with the file:

1. `default` is a *fallback*, not a universal role. Step 3 substitutes it
   only when a user matched zero AppRoles, and prod's `default` carries no
   `jwtRoleMappings`, so granting there reaches only unmapped users. The
   "GA = one grant to `default`, no redeploy" framing that appeared in
   several comments was wrong.
2. The roles-UI limitation above, recorded where someone would see it before
   routing a new grant through this axis.

The algorithm list in that docstring is renumbered to match the code's own
step comments, so the "Step 3" reference is unambiguous.

Also corrects both `feature_flags.py` docstrings, which still described the
now-deleted capability as the companion "who may use it" control:
`skills_enabled` points at a role's `grantedSkills`; `scheduled_runs_enabled`
notes the flag is now the only control and the routes are deliberately
ungated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d-capabilities-module

refactor(rbac): delete the dead capabilities module
Feature release delivering Skills v2 — skills redesigned from tool-binding
containers into pure, portable knowledge bundles that Agents load on demand,
with a user-authored tier so any signed-in user can create their own. Skills
ship enabled by default. Also completes the session-metadata static-sort-key
migration (issue #175 Phases 2-3) and consolidates the two artifact
tool-catalog rows into a single "Artifacts" toggle.

- Skills v2 (#679-#685, #692): pure knowledge bundles in agentskills.io S3
  format with a SKILL.md projection, progressive disclosure L1-L3 via the new
  read_skill_file tool, the Strands AgentSkills plugin replacing the bespoke
  SkillAgent, a user-authored tier at /skills/mine/*, a chat picker, and
  invoke-through so a shared Agent resolves its skills for the recipient.
  Skills no longer bind tools — allowedTools persists as advisory metadata
  and never grants. The `skills` capability gate was removed as inoperable:
  it could not be granted from the admin roles UI at all.
- Session metadata (#677, #678): cold-tail backfill script plus a GSI-only
  read contraction gated on its completion marker, failing open to dual-read.
- Artifacts (#689): two catalog rows collapsed to one, with a backfill that
  promotes grants before retiring the old id.

Three backfill scripts must be run per environment; see RELEASE_NOTES.md.
No new AWS resources and no dependency changes. Skills activate on the
backend.yml deploy itself, since unset SKILLS_ENABLED now reads as enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@philmerrell
philmerrell requested a review from a team July 19, 2026 18:19
self._invalidate(skill_id)

logger.info(
f"User {user.email} created skill: {skill_id}",
Comment on lines +183 to +187
extra={
"event": "user_skill_created",
"skill_id": skill_id,
"owner_user_id": user.user_id,
},
self._invalidate(skill_id)

logger.info(
f"User {user.email} updated skill: {skill_id}",
Comment on lines +213 to +218
extra={
"event": "user_skill_updated",
"skill_id": skill_id,
"owner_user_id": user.user_id,
"changes": list(updates.keys()),
},
self._invalidate(skill_id)

logger.info(
f"User {user.email} deleted skill: {skill_id}",
Comment on lines +242 to +246
extra={
"event": "user_skill_deleted",
"skill_id": skill_id,
"owner_user_id": user.user_id,
},
import logging
import os
import time
from typing import Any, Dict, Optional, Tuple
try:
resp = table.get_item(Key={"PK": _MIGRATION_MARKER_PK, "SK": _MIGRATION_MARKER_SK})
if (resp.get("Item") or {}).get("complete"):
_migration_complete_cache = True
from botocore.exceptions import ClientError

from .models import SkillDefinition, SkillStatus, UserSkillPreference
from .models import SYSTEM_OWNER_ID, SkillDefinition, SkillStatus, UserSkillPreference
@philmerrell
philmerrell merged commit aaa9509 into main Jul 19, 2026
9 checks passed
@philmerrell
philmerrell deleted the release/1.8.0 branch July 19, 2026 18:34
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.

6 participants