* feat: add Ochre console Playground page
A Converse chat surface with model picker, system prompt, inference params,
optional guardrail, per-turn tokens, raw request/response JSON, and copy-as-CLI.
The transport lives in one module so a later ConverseStream swap edits a single
file; the message list appends and mutates the last turn in place. A cold model
degrades to a retryable warming-up turn instead of an error.
* feat: pin the playground composer, send on Enter, render markdown
The chat composer sat below the message list in page flow, so it slid down as
the conversation grew and the reader had to scroll to reach it. Bound the
chat + composer to a fixed-height column: the message pane scrolls internally
and autoscrolls to the newest turn while the composer stays pinned.
Enter now sends (Shift+Enter inserts a newline), and assistant replies render
markdown — lists, inline code, code blocks, and links — instead of raw text.
Adds the accessors for the per-resource instance record key space, over both
buckets that carry instance records. Nothing calls them yet.
The prefix is disjoint from the three it replaces: List and DeletePrefix match
on a plain string prefix rather than on NATS subject tokens, so i/ shares a
bucket with instance., node. and terminated. without colliding with any of them.
mutateVM and listVMs become generic so the record accessors reuse them rather
than carrying a second copy.
InstanceRecord is resource.Object[InstanceSpec, InstanceStatus], the persisted
form of an instance under the per-resource key space the re-key introduces.
Nothing reads or writes it yet: the boundary is settled and tested before the
keys move onto it.
The wrappers around the EBS and ENI request slices are dropped on the way in
and rebuilt on the way out, so the record does not carry a mutex.
Instance and Reservation are carried whole. They are SDK types returned
verbatim by DescribeInstances and hold both halves in one object, so the
boundary does not hold there and the comment says so.
Object[Spec, Status] separates what an operator asked for from what a node
observed, with the identity and lifecycle bookkeeping common to both.
Generation is bumped only through MutateSpec so NeedsSync means something, and
ObserveGeneration records the generation the node acted on rather than the
current one, so a spec change landing mid-reconcile is not marked done.
No ARN or tag helpers: the arn package already builds ARNs per service, and the
tag helpers in utils already cover the map every record carries.
StateReason was cleared by ModifyInstanceAttribute, so the API wrote a field
the node owns. Move the clear onto the launch path.
ExtraHostfwd held the requested guest port and the bound host port in one map,
with zero standing in for both 'not bound yet' and a real value. Split it into
HostfwdPorts and HostfwdPortMap, rebuilt per launch, and fold legacy records on
decode.
The command an instance was last handled with was stored on the record, but
only stop_instance and delete_instance were ever read back. Replace them with
DesiredState and DeletionTimestamp, which say what the read sites actually
mean, and drop the other twelve booleans.
Legacy records are folded on decode so an operator-stopped instance is not
relaunched on upgrade.
A staged self-host model is granted to every account without a per-account
grant, making the admin import the only v1 gate. Wrap the access resolver so
every gate opens together; the admin catalog reports self-host state
weights-first. Per-account grants remain as a dormant seam.
Operator-only catalog page listing every model with modality, streaming,
price, VRAM/instance, co-serve group, and availability with a fix-affordance
hint per reason. Reads the ListOchreCatalog admin action, mirroring the
existing admin-service query pattern.
Expose per-model availability and reason on an operator-only ListOchreCatalog
action for the console, keeping the tenant ListFoundationModels omission
intact. Remove the unbuilt provider-direct entry from the v1 catalog.
The server queues the API audit advisory after the reply the caller
waits on, so subscribing once the bucket exists could still catch the
create the first call made. Watch from the start and account for it.
The handler records its request point after it replies, so asserting as
soon as Request returns could read the metric before it landed. Poll for
the outcome the way the sibling drain test does.
The 40ms apply-params timeout was shared by the tests that expect the
agent to answer and those that expect it not to, so a parallel coverage
run could miss the reply. Only the unreachable-agent cases keep the
short budget.
* feat: semantic topic detection for guardrails via embedding similarity
Guardrail topic policy previously only blocked on an exact word-boundary
match against a denied topic's name or listed examples, so a paraphrase or
synonym for a blocked topic sailed straight through. This adds an
embedding-similarity check: each denied topic's name, definition, and
examples are embedded and cached, incoming text is embedded and compared by
max cosine similarity against those phrase vectors, and a topic blocks when
that similarity clears a conservative threshold. The literal exact-match
check still runs first and always blocks on its own. If no embedder is
configured, or an embed call fails, the check falls back to the existing
literal match alone rather than ever failing open.
* fix: fail closed when guardrail topic embedding is unavailable
The topic policy's semantic check silently fell back to its literal-only
matcher whenever the embedder was configured but temporarily unreachable
(for example during model cold-start), so a paraphrase of a denied topic
could pass through unblocked while the embedding endpoint was down. Now that
case is distinguished from an unconfigured embedder: if literal matching
already caught the request, that block still wins outright. Otherwise, when
the embedder is wired but the embed call itself fails, the guardrail check
returns an error and the caller aborts the request rather than letting
unverified content through.
* feat: converge DNS on change rather than on a timer
The drift backstop now watches the four stores dnsDesiredSet reads: instance state, ELBv2 load balancers, and the per-account EKS and RDS buckets. Only the trigger changes; the leader election, computeBatch and the publish path are untouched.
The interval becomes the backstop for drift the buckets cannot report, such as a record edited directly in the zone object, rather than the path by which a lifecycle event reaches DNS.
* test: cover the DNS watch wiring and reconciler run path
ListUsers, ListRoles, ListInstanceProfiles and ListGroupsForUser built their
results from a nil slice when the filtering loop matched nothing. xmlutil
drops a nil slice entirely rather than emitting an empty element, so each
response omitted a member its output shape marks required.
Each operation's empty-bucket shortcut already returned an empty non-nil
slice, so only the filtering path was affected. Start those slices empty and
non-nil too.
ListAttachedGroupPolicies, ListAttachedRolePolicies, ListPolicies and
ListAttachedUserPolicies declare no required output members, so their nil
slices are legal and are left alone.
DescribeImages projected BootMode through aws.String unconditionally, so an
AMI recording none emitted "", which is outside the model enum. RegisterImage
takes BootMode as an optional input, so this is the normal path for an AMI
registered without one, not stale data. Set the member only when present.
ListAccessKeys and ListGroups built their results from a nil slice when
nothing matched. xmlutil drops a nil slice entirely rather than emitting an
empty element, so the response lost a member the model marks required. Start
both slices empty and non-nil.
Integration conformance drops from 18 violations to 15; the remainder are the
EC2 AccessDenied error-code findings, which are unrelated.
The conformance subtest asserting the clamp ran against a provider holding
no volumes, so len(resp.Volumes) <= MaxListResults held whatever PageSize
did. Removing the clamp branch left the suite green.
Add a unit test on PageSize covering above the cap, at it, below it, and at
or below zero, and give the conformance subtests a fixture so the claim they
keep, that an over-cap request returns a page rather than an error, can fail.
buildInstanceStatus read vm.EC2StateCodes directly, so a StateError instance
surfaced the non-AWS name "error" while DescribeInstances returned "stopped"
for the same instance.
Ubuntu 22.04 was accepted by detect_os but the runtime apt list has never
resolved on jammy, which ships dhcpcd5 rather than dhcpcd-base. No CI cell
or E2E matrix installs on it, and the install docs already claim Ubuntu
26.04 and Debian 13 only, so drop the claim instead of carrying a
jammy-conditional package branch for an untested suite.
Watch is used zero times in spinifex against 33 ticker loops. This adds the primitive on Bucket, which already owns the memoised handle, plus the watch/debounce/resync/re-establish loop, so each converted caller changes only its trigger.
Watches are established before the first pass: a change landing between the two would otherwise wait for the next resync, since the pass that would have seen it ran before the watch existed.
Service.bucket() resolved a JetStream client and did a get-or-create round trip on every KV operation; kvstore.Store memoises the handle per Service.
Add Store.Purge, since the endpoint records are deleted and re-created under the same key and Create is the single-writer claim: a delete marker is not the same starting state as an absent key.
Persist ingestion progress and bound per-document work with a timeout so a
job on a dead backend fails fast and visibly instead of sitting silently
IN_PROGRESS; add StopIngestionJob so a stuck job can be cancelled; map a
backend-down Retrieve/RetrieveAndGenerate error to 503 instead of an opaque
500.