Package canned-prompts for Railiance

Registers the repo with State Hub (agents / practice, prefix RCP-WP) and fills
in the rapp shape.

declarations/rapp.yaml declares a manifest-managed platform service owned by
canned-prompts, bound to rail-kubernetes and reef-railiance, with rollout,
smoke and rollback contracts.

The image pin says `pending-publication` rather than carrying a placeholder
digest. The image builds and was verified locally (canned-prompts
CANP-WP-0006-T06) but has never been pushed, so no registry digest exists. A
placeholder shaped like a real digest would be worse than a sentinel: it could
be mistaken for something deployable.

manifests/ follows the rapp-sbom-nexus shape: namespace labelled for the
postgres client, external secrets from OpenBao, a migration Job, and the
runtime Deployment with a ClusterIP-only Service, dedicated ServiceAccount and
default-deny plus runtime NetworkPolicies.

Three choices worth stating. Credentials arrive as mounted files, never env
vars — an env var holding a password is visible in kubectl describe, in crash
dumps, and to anything that can read /proc. Migrations run as a Job rather than
at start-up, so a schema rollback stays separate from a code rollback and
replicas do not race. Liveness points at /healthz, which checks only that the
process is up: pointing it at a database-dependent path would restart every
replica during a database blip.

Egress is PostgreSQL and DNS only. A package arrives by publish; the registry
never reaches out, so it is given no path to.

tools/smoke.sh checks what only the cluster can answer and calls
canned-prompts' service/tools/smoke.py for health and migration head, rather
than holding a second opinion about whether the service is healthy.

RCP-WP-0002 carries the two operator actions that block a first rollout —
publishing the image and provisioning database roles — and records
per-publisher identity as a decision belonging upstream, which this repo must
not paper over with cluster configuration implying finer control than exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
This commit is contained in:
tegwick 2026-09-06 21:44:17 +02:00
parent 657ce841fe
commit 5a0f4cb5c8
14 changed files with 991 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# state-hub: track .claude/rules
.claude/*
!.claude/rules/
!.claude/rules/*.md
# Rendered output is generated at apply time, never committed by hand.
.rendered/

20
.repo-classification.yaml Normal file
View file

@ -0,0 +1,20 @@
repo_classification:
standard: Repo Classification Standard
version: '1.1'
classified_at: '2026-09-06'
classified_by: claude
category: tooling
domain: agents
secondary_domains:
- infotech
capability_tags:
- deployment
- operations
- configuration
- observability
business_stake:
- technology
- operations
business_mechanics:
- operation
- control

203
AGENTS.md Normal file
View file

@ -0,0 +1,203 @@
# rapp-canned-prompts — Agent Instructions
## Repo Identity
**Purpose:** Package and operate the canned-prompts hosted registry and index on Railiance, without moving product ownership out of canned-prompts.
**Domain:** agents
**Repo slug:** rapp-canned-prompts
**Topic ID:** `c1d199b6-55ee-4db6-b49e-257a9f0f15ac`
**Workplan prefix:** `RCP-WP-`
---
## State Hub Integration
The Custodian State Hub tracks work across all domains. Codex uses HTTP REST and
the `statehub` CLI by default. MCP is opt-in because the current Codex MCP bridge
adds severe call latency; the full administrative MCP surface remains available
to clients that need it.
| Context | URL |
|---------|-----|
| Local workstation | `http://127.0.0.1:8000` |
| Remote via tunnel | `http://127.0.0.1:18000` |
| Optional local edge relay | http://127.0.0.1:18080 |
When an operator has enabled the edge relay, set API_BASE to the relay URL.
Queueable writes return an explicit queued receipt if the central hub is
unreachable. Treat that as pending local evidence, then ask the operator to run
statehub outbox status/replay after connectivity returns.
Codex workspace-write sandboxes need network access enabled to reach the host's
loopback listener. Bootstrap this once with `make -C ~/state-hub configure-codex`
and restart Codex. The canonical REST health endpoint is `/state/health`, not
`/health`. If a sandboxed loopback probe fails, retry it with escalated execution
before declaring State Hub unavailable; a managed Codex permission profile may
still enforce isolated networking. Experimental MCP can be enabled explicitly
with `make -C ~/state-hub configure-codex WITH_MCP=1`.
### Orient at session start
```bash
# Offline brief — works without hub connection
cat .custodian-brief.md
# Active workplans for this domain
curl -s "http://127.0.0.1:8000/workplans/?topic_id=c1d199b6-55ee-4db6-b49e-257a9f0f15ac&status=active" \
| python3 -m json.tool
# Check inbox
curl -s "http://127.0.0.1:8000/messages/?to_agent=rapp-canned-prompts&unread_only=true" \
| python3 -m json.tool
```
Mark a message read:
```bash
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
### Log progress (required at session close)
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{
"summary": "what was done",
"event_type": "note",
"author": "codex",
"workplan_id": "<uuid>",
"task_id": "<uuid>"
}'
```
Omit `workplan_id` / `task_id` when not applicable.
### Update task status
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"status": "progress"}'
# values: wait | todo | progress | done | cancel
```
### Flag a task for human review
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"needs_human": true, "intervention_note": "reason"}'
```
---
## Session Protocol
**Start:**
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
2. Check inbox: `GET /messages/?to_agent=rapp-canned-prompts&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
**During work:**
- Update task statuses in workplan files as tasks progress
- Record significant decisions via `POST /decisions/`
**Close:**
1. Update workplan file task statuses to reflect progress
2. If finishing a workplan: hand off **residuals** as live work records first
(intake with `origin: residual` + `origin_ref: <WP-id>`, or a next workplan /
decision / engagement). Do not park leftovers only in prose or `SCOPE.md`.
Canon: `the-custodian/canon/standards/work-record-types_v0.1.md` § Residuals.
3. Log: `POST /progress/` with a summary of what changed (name handoff ids)
4. After workplan file changes, run:
```bash
uv run --project ~/repo-manager rmgr sync --path . --push
```
This assigns only missing deterministic identifiers, verifies the pushed
Forgejo commit and `primary/railliance01`, then requests one central
reconciliation. A queued receipt is pending evidence; rerun after
connectivity returns. Use `statehub fix-consistency` for a separate deep audit.
---
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
---
## Workplan Convention (ADR-001)
Work items originate as files in this repo — not in the hub. The hub is a
read/cache/index layer that rebuilds from files.
**File location:** `workplans/RCP-WP-NNNN-<slug>.md`
**Archived location:** finished workplans may move to
`workplans/archived/YYMMDD-RCP-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
the completion/archive date; the frontmatter `id` does not change.
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
`workplans/ADHOC-YYYY-MM-DD.md`, workplan id
`RCP-WP-ADHOC-YYYY-MM-DD`, and task ids
`RCP-WP-ADHOC-YYYY-MM-DD-T01`, etc. `RCP-WP` includes its final `-WP`
token. Unqualified historic `ADHOC-*` ids are grandfathered and must not be
copied into new records. Use this only for low-risk work completed directly;
create a normal workplan for anything needing analysis, design, approval,
dependencies, or multiple phases.
**Frontmatter:**
```yaml
---
id: RCP-WP-NNNN
type: workplan
title: "..."
domain: agents
repo: rapp-canned-prompts
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
topic_slug: ...
created: "YYYY-MM-DD"
updated: "YYYY-MM-DD"
state_hub_workstream_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
---
```
Use `proposed` for a new draft, `ready` after review against current repo
state, and `finished` after implementation. `stalled` and `needs_review` are
derived health labels, not frontmatter statuses.
**Terminology:** workplan is the fleet term; `workstream` appears only in legacy
API/MCP/frontmatter bridges until `STATE-WP-0069` retires them — see
`the-custodian/canon/standards/workplan-terminology-fleet_v0.1.md`.
**Task block format** (one per `##` section):
```
## Task Title
` ` `task
id: RCP-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
` ` `
Task description text.
```
Status progression: `todo``progress``done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
**Residuals when finishing:** actionable leftovers become live work records
before `status: finished` — usually an intake (`origin: residual`,
`origin_ref: RCP-WP-NNNN`) or a spawned workplan. Residual is a *role*,
not a kind. Fleet list lives on State Hub, not in `SCOPE.md`.
To create a new workplan:
1. Write the file following the format above
2. Run `uv run --project ~/repo-manager rmgr sync --path . --push`.
3. Run `statehub fix-consistency` only when a separate deep audit is needed.

29
INTENT.md Normal file
View file

@ -0,0 +1,29 @@
# Intent — rapp-canned-prompts
Package and operate the `canned-prompts` hosted registry and index on
Railiance, without moving product ownership into an operations repository.
This package makes deployment reproducible: an immutable image pin, a private
ClusterIP Service, credentials mounted as files from the platform broker, a
one-shot schema migration under a separate owner role, least-privilege
NetworkPolicies, live verification, and digest rollback.
Format semantics, package validation, API compatibility, the schema and its
migrations, and image publication remain owned by `canned-prompts`. PostgreSQL
topology, database isolation, backups and credential issuance remain owned by
`rapp-postgres` and the platform credential broker.
## What this repo decides
- how the workload is composed, pinned, rolled out and rolled back;
- what "healthy" means at the cluster level, and how it is verified;
- the network posture: what may reach the service, and what it may reach.
## What it does not decide
- what a valid prompt package is, or what any endpoint means — `canned-prompts`;
- database topology, backup or credential rotation — `rapp-postgres` and the broker;
- who may publish. The service's identity model is a single shared token
proving "the operator". Per-publisher identity is an open question in
`canned-prompts`, and this repo must not paper over it with cluster
configuration that implies finer control than exists.

56
SCOPE.md Normal file
View file

@ -0,0 +1,56 @@
# SCOPE
## One-liner
Package and operate the `canned-prompts` hosted registry and index on
Railiance, without moving product ownership into an operations repository.
## In Scope
- `declarations/rapp.yaml` — the managed-workload declaration: composition,
image pin, rollout, smoke and rollback contracts.
- `manifests/` — namespace, external secrets, migration Job, and the runtime
Deployment, Service, ServiceAccount and NetworkPolicies.
- `tools/smoke.sh` — deployment-level verification, delegating the
service-level half to `canned-prompts`.
- Rollout, rollback and the network posture.
## Out of Scope
- What a valid prompt package is, what any endpoint means, the schema and its
migrations, and image publication — all `canned-prompts`.
- PostgreSQL topology, isolation, backups, credential issuance — `rapp-postgres`
and the platform credential broker.
- Who may publish. The service's identity is a single shared token proving
"the operator"; per-publisher identity is an open question upstream, and this
repo must not imply finer control than exists.
## Current State
**Draft.** The package is written and its manifests parse, but nothing is
deployed and `readiness_state` is `draft`. Two things block a first rollout,
both operator actions rather than authoring ones:
- the image has never been published, so no registry digest exists to pin
(`declarations/rapp.yaml` says `pending-publication` rather than carrying a
placeholder that could be mistaken for something deployable);
- database roles and OpenBao credentials are not provisioned.
`workplans/RCP-WP-0002-first-deployment.md` carries both.
## Verification
This repo builds nothing.
```bash
for f in manifests/*.yaml; do python3 -c "import yaml,sys; list(yaml.safe_load_all(open('$f')))"; done
bash -n tools/smoke.sh
NS=canned-prompts ./tools/smoke.sh # against a live deployment
```
## Getting Oriented
- Intent and boundaries: `INTENT.md`
- The declaration: `declarations/rapp.yaml`
- What the service is: `canned-prompts/service/README.md`
- Deployment guide: `repo-manager/docs/RailianceAppDeploymentGuide.md`

20
WORK-RECORDS.md Normal file
View file

@ -0,0 +1,20 @@
# Work Records — rapp-canned-prompts
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
> stage 3). Do not edit by hand — edit the source file/block listed for
> each record and re-run fix-consistency to refresh this index. Archived
> workplans are omitted; closed decisions/intakes/engagements stay listed
> so recently-resolved work is still visible. [auto]
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | RCP-WP-0001 | finished | — | workplans/RCP-WP-0001-statehub-bootstrap.md |
| workplan | RCP-WP-0002 | proposed | — | workplans/RCP-WP-0002-first-deployment.md |
| task | RCP-WP-0001-T01 | done | — | workplans/RCP-WP-0001-statehub-bootstrap.md |
| task | RCP-WP-0001-T02 | done | — | workplans/RCP-WP-0001-statehub-bootstrap.md |
| task | RCP-WP-0001-T03 | done | — | workplans/RCP-WP-0001-statehub-bootstrap.md |
| task | RCP-WP-0002-T01 | todo | — | workplans/RCP-WP-0002-first-deployment.md |
| task | RCP-WP-0002-T02 | todo | — | workplans/RCP-WP-0002-first-deployment.md |
| task | RCP-WP-0002-T03 | todo | — | workplans/RCP-WP-0002-first-deployment.md |
| task | RCP-WP-0002-T04 | todo | — | workplans/RCP-WP-0002-first-deployment.md |
| task | RCP-WP-0002-T05 | wait | — | workplans/RCP-WP-0002-first-deployment.md |

65
declarations/rapp.yaml Normal file
View file

@ -0,0 +1,65 @@
kind: managed-workload-package
repo_family: rapp
rapp_id: rapp-canned-prompts
repo: rapp-canned-prompts
ownership_repo: canned-prompts
contract_version: 1.0.0
readiness_state: draft
workload_identity:
name: canned-prompts
package_type: manifest-managed-platform-service
data_classification: internal
criticality: medium
primary_rail: rail-kubernetes
supported_rails:
- rail-kubernetes
bound_reefs:
- reef-railiance
runtime_dependencies:
- kubernetes-api
- openbao-database-secrets-engine
composition:
purpose: >-
Package and operate the canned-prompts hosted registry and index on
Railiance. Format semantics, package validation, API compatibility, the
schema and its migrations, and image publication remain owned by
canned-prompts. PostgreSQL topology, database isolation, backups and
credential issuance remain owned by rapp-postgres and the platform
credential broker.
member_repos:
- repo: rapp-canned-prompts
role: managed runtime package
deployables:
- canned-prompts
upstream_components:
- name: canned-prompts
source: forgejo.coulomb.social/coulomb/canned-prompts
# NOT YET PUBLISHED. The image builds and was verified locally
# (CANP-WP-0006-T06), but has never been pushed, so no registry digest
# exists to pin. A placeholder shaped like a digest would be worse than
# this sentinel: it could be mistaken for something deployable.
# RCP-WP-0001-T01 replaces this with the real digest.
version: pending-publication
rollout_contract:
default_mode: kubectl-server-side-apply
smoke_contract:
required:
- state-health-ok
- migration-at-head
- external-secrets-ready
- private-service-only
- networkpolicies-present
- live-image-digest-match
rollback_contract:
order:
- previous-immutable-image-digest
- apply-reviewed-git-revision
source_documents:
- repo: canned-prompts
path: CannedPromptFormat.md
- repo: canned-prompts
path: service/README.md
- repo: canned-prompts
path: workplans/CANP-WP-0006-hosted-registry-service.md
- repo: repo-manager
path: docs/RailianceAppDeploymentGuide.md

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: Namespace
metadata:
name: canned-prompts
labels:
app.kubernetes.io/name: canned-prompts
app.kubernetes.io/part-of: canned-prompts
railiance.io/postgres-client: platform-pg-2

View file

@ -0,0 +1,90 @@
# Two credentials, deliberately. The runtime role can read and write rows; the
# migration role owns the schema. A service that can ALTER its own tables at
# runtime turns any code defect into a schema defect.
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: openbao-canned-prompts-database
labels:
app.kubernetes.io/name: canned-prompts
railiance-platform/component: external-secrets
spec:
conditions:
- namespaces:
- canned-prompts
provider:
vault:
server: http://openbao.openbao.svc:8200
path: database
version: v1
auth:
tokenSecretRef:
name: openbao-canned-prompts-eso-token
namespace: external-secrets
key: token
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: canned-prompts-postgres-runtime
namespace: canned-prompts
spec:
refreshInterval: 5m
secretStoreRef:
kind: ClusterSecretStore
name: openbao-canned-prompts-database
target:
name: canned-prompts-postgres-runtime
creationPolicy: Owner
deletionPolicy: Retain
template:
engineVersion: v2
data:
url: >-
postgresql+psycopg://{{ .username | urlquery }}:{{ .password | urlquery }}@platform-pg-2-rw.databases.svc.cluster.local:5432/canned_prompts?sslmode=require
dataFrom:
- extract:
key: creds/canned-prompts-runtime
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: canned-prompts-postgres-migration
namespace: canned-prompts
spec:
refreshInterval: 5m
secretStoreRef:
kind: ClusterSecretStore
name: openbao-canned-prompts-database
target:
name: canned-prompts-postgres-migration
creationPolicy: Owner
deletionPolicy: Retain
template:
engineVersion: v2
data:
url: >-
postgresql+psycopg://{{ .username | urlquery }}:{{ .password | urlquery }}@platform-pg-2-rw.databases.svc.cluster.local:5432/canned_prompts?sslmode=require
dataFrom:
- extract:
key: creds/canned-prompts-migration
---
# The publish token. Absent, the service is read-only — which is the correct
# posture until per-publisher identity exists (canned-prompts service/auth.py).
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: canned-prompts-publish-token
namespace: canned-prompts
spec:
refreshInterval: 15m
secretStoreRef:
kind: ClusterSecretStore
name: openbao-canned-prompts-database
target:
name: canned-prompts-publish-token
creationPolicy: Owner
deletionPolicy: Retain
dataFrom:
- extract:
key: creds/canned-prompts-publish

64
manifests/migration.yaml Normal file
View file

@ -0,0 +1,64 @@
# Schema migration as a Job, not an init container and not a start-up hook.
# Running migrations at start-up races between replicas and couples a rollback
# of the code to a rollback of the schema. The Job name carries the target
# revision so a re-apply at the same revision is a no-op rather than a rerun.
apiVersion: batch/v1
kind: Job
metadata:
name: canned-prompts-schema-migration-0002
namespace: canned-prompts
labels:
app.kubernetes.io/name: canned-prompts-migration
app.kubernetes.io/component: migration
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app.kubernetes.io/name: canned-prompts-migration
spec:
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: migrate
# REPLACE on first publication — see declarations/rapp.yaml.
image: forgejo.coulomb.social/coulomb/canned-prompts:pending-publication
command: ["alembic"]
args: ["upgrade", "head"]
workingDir: /app
env:
# The migration role owns the schema; the runtime role does not.
- name: CANNED_PROMPTS_DATABASE_URL_FILE
value: /var/run/secrets/postgres-migration/url
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: postgres-migration
mountPath: /var/run/secrets/postgres-migration
readOnly: true
volumes:
- name: postgres-migration
secret:
defaultMode: 0440
secretName: canned-prompts-postgres-migration
items:
- key: url
path: url

170
manifests/runtime.yaml Normal file
View file

@ -0,0 +1,170 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: canned-prompts
namespace: canned-prompts
labels:
app.kubernetes.io/name: canned-prompts
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: canned-prompts
template:
metadata:
labels:
app.kubernetes.io/name: canned-prompts
app.kubernetes.io/part-of: canned-prompts
spec:
automountServiceAccountToken: false
serviceAccountName: canned-prompts
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: canned-prompts
# REPLACE on first publication — see declarations/rapp.yaml.
image: forgejo.coulomb.social/coulomb/canned-prompts:pending-publication
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
env:
# The URL arrives as a mounted file, never as an env var: an env var
# holding a password shows up in `kubectl describe`, in crash dumps,
# and to anything that can read /proc.
- name: CANNED_PROMPTS_DATABASE_URL_FILE
value: /var/run/secrets/postgres-runtime/url
- name: CANNED_PROMPTS_PUBLISH_TOKEN_FILE
value: /var/run/secrets/publish/token
- name: CANNED_PROMPTS_TENANT
value: railiance
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
readinessProbe:
httpGet:
path: /readyz
port: http
periodSeconds: 5
livenessProbe:
# /healthz deliberately checks only that the process is up. Pointing
# liveness at a database-dependent path would restart every replica
# during a database blip, turning a brief outage into an outage plus
# a thundering herd.
httpGet:
path: /healthz
port: http
periodSeconds: 20
startupProbe:
httpGet:
path: /readyz
port: http
failureThreshold: 30
periodSeconds: 2
resources:
requests:
cpu: 25m
memory: 96Mi
limits:
cpu: 500m
memory: 384Mi
volumeMounts:
- name: postgres-runtime
mountPath: /var/run/secrets/postgres-runtime
readOnly: true
- name: publish
mountPath: /var/run/secrets/publish
readOnly: true
volumes:
- name: postgres-runtime
secret:
defaultMode: 0440
secretName: canned-prompts-postgres-runtime
items:
- key: url
path: url
- name: publish
secret:
defaultMode: 0440
secretName: canned-prompts-publish-token
optional: true
items:
- key: token
path: token
---
apiVersion: v1
kind: Service
metadata:
name: canned-prompts
namespace: canned-prompts
spec:
# ClusterIP only. No Ingress, no LoadBalancer: this registry is reachable
# from inside the cluster and nowhere else, which is what
# `private-service-only` in the smoke contract asserts.
type: ClusterIP
selector:
app.kubernetes.io/name: canned-prompts
ports:
- name: http
port: 8000
targetPort: http
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: canned-prompts
namespace: canned-prompts
automountServiceAccountToken: false
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: canned-prompts-default-deny
namespace: canned-prompts
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: canned-prompts-runtime
namespace: canned-prompts
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: canned-prompts
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 8000
egress:
# PostgreSQL and DNS only. The service fetches nothing: a package arrives
# by publish, never by the registry reaching out, so it needs no egress to
# the internet and is not given any.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: databases
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53

58
tools/smoke.sh Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Deployment-level smoke checks for rapp-canned-prompts.
#
# The service-level half lives in canned-prompts (`service/tools/smoke.py`) and
# is called from here rather than reimplemented — the deployment should not hold
# a second opinion about whether the service is healthy.
#
# What is checked here is what only the cluster can answer: that secrets
# materialized, that the Service is private, that NetworkPolicies exist, and
# that the running image is the digest this repo pins.
set -euo pipefail
NS=${NS:-canned-prompts}
EXPECT_DIGEST=${EXPECT_DIGEST:-$(grep -A1 'name: canned-prompts$' declarations/rapp.yaml | grep 'version:' | awk '{print $2}' || true)}
FAILED=0
check() { # name, condition-output
if [ "$2" = "ok" ]; then printf 'PASS %s\n' "$1"; else printf 'FAIL %s %s\n' "$1" "$2"; FAILED=$((FAILED+1)); fi
}
# external-secrets-ready
for s in canned-prompts-postgres-runtime canned-prompts-postgres-migration; do
if kubectl -n "$NS" get secret "$s" >/dev/null 2>&1; then check "external-secrets-ready:$s" ok
else check "external-secrets-ready:$s" "secret absent"; fi
done
# private-service-only — a ClusterIP and no Ingress pointing at it
TYPE=$(kubectl -n "$NS" get svc canned-prompts -o jsonpath='{.spec.type}' 2>/dev/null || echo missing)
[ "$TYPE" = "ClusterIP" ] && check "private-service-only:type" ok || check "private-service-only:type" "type=$TYPE"
INGRESS=$(kubectl -n "$NS" get ingress -o name 2>/dev/null | wc -l)
[ "$INGRESS" = "0" ] && check "private-service-only:no-ingress" ok || check "private-service-only:no-ingress" "$INGRESS ingress objects"
# networkpolicies-present — a default-deny plus the runtime policy
NP=$(kubectl -n "$NS" get networkpolicy -o name 2>/dev/null | wc -l)
[ "$NP" -ge 2 ] && check "networkpolicies-present" ok || check "networkpolicies-present" "$NP policies"
# live-image-digest-match
LIVE=$(kubectl -n "$NS" get deploy canned-prompts -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || echo missing)
case "$EXPECT_DIGEST" in
""|pending-publication)
check "live-image-digest-match" "no digest pinned yet (declarations/rapp.yaml says pending-publication)" ;;
*) [ "${LIVE##*@}" = "$EXPECT_DIGEST" ] && check "live-image-digest-match" ok \
|| check "live-image-digest-match" "live=$LIVE expected=$EXPECT_DIGEST" ;;
esac
# state-health-ok and migration-at-head, from the owning repo's checker.
SERVICE_SMOKE=${SERVICE_SMOKE:-$HOME/canned-prompts/service/tools/smoke.py}
if [ -f "$SERVICE_SMOKE" ]; then
echo "--- service-level (${SERVICE_SMOKE}) ---"
kubectl -n "$NS" port-forward svc/canned-prompts 18000:8000 >/dev/null 2>&1 &
PF=$!; trap 'kill $PF 2>/dev/null || true' EXIT; sleep 3
python3 "$SERVICE_SMOKE" --base http://127.0.0.1:18000 --expect-migration "${EXPECT_MIGRATION:-0002}" || FAILED=$((FAILED+1))
else
check "service-level-checks" "canned-prompts checkout not found at $SERVICE_SMOKE"
fi
[ "$FAILED" -eq 0 ] || { echo; echo "$FAILED check(s) failed" >&2; exit 1; }
echo; echo "all deployment checks passed"

View file

@ -0,0 +1,67 @@
---
id: RCP-WP-0001
type: workplan
title: "Bootstrap State Hub integration"
domain: agents
repo: rapp-canned-prompts
status: finished
owner: codex
topic_slug: practice
created: "2026-09-06"
updated: "2026-09-06"
---
# Bootstrap State Hub integration
Package and operate the canned-prompts hosted registry and index on Railiance, without moving product ownership out of canned-prompts.
## Review Generated Integration Files
```task
id: RCP-WP-0001-T01
status: done
priority: high
```
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
Replace generated placeholders with repo-specific facts where needed.
Done: `INTENT.md` written to the rapp shape — what this repo decides, and what
it explicitly does not. `.repo-classification.yaml` added (tooling / agents,
validated against the canon allowed-values). The unresolved
`{CREDENTIAL_ROUTING}` token the generator leaves behind was removed; it is a
template defect already reported to `state-hub`.
## Verify Local Developer Workflow
```task
id: RCP-WP-0001-T02
status: done
priority: high
```
Identify the repo's install, test, lint, build, and run commands. Add or refine
those commands in the agent instructions so future coding sessions can verify
changes confidently.
Done. This repo builds nothing: it packages an image built in `canned-prompts`.
Verification is `tools/smoke.sh` against a live deployment, plus YAML parse
checks over `manifests/`. Recorded in `SCOPE.md`.
## Seed First Real Workplan
```task
id: RCP-WP-0001-T03
status: done
priority: medium
```
Done: `workplans/RCP-WP-0002-first-deployment.md`.
Create the first implementation workplan for the repository's most important
next change. After workplan file updates, run the sync locally from this repo
checkout:
```bash
statehub fix-consistency
```

View file

@ -0,0 +1,134 @@
---
id: RCP-WP-0002
type: workplan
title: "First deployment of canned-prompts on Railiance"
domain: agents
repo: rapp-canned-prompts
status: proposed
owner: codex
topic_slug: practice
created: "2026-09-06"
updated: "2026-09-06"
state_hub_workstream_id: "11874f32-ac36-5bb9-a0a5-e7a259f5972c"
---
# First deployment of canned-prompts on Railiance
The package is written and validated; what remains needs credentials and a
published image, which are operator actions rather than authoring ones.
`readiness_state` is `draft` and stays there until T04 produces evidence.
Topology is not readiness (ADR-0006): binding this rapp to a reef does not make
it deployed.
## Publish the image and pin its digest
```task
id: RCP-WP-0002-T01
status: todo
priority: high
state_hub_task_id: "3e7faf50-8fe9-5ef3-8a75-1d9e586d6c0f"
```
`declarations/rapp.yaml` carries `version: pending-publication`, and both
manifests carry a matching tag. This is deliberate: a placeholder shaped like a
real digest could be mistaken for something deployable, and a rapp that pins
nothing is a stub that tells the fleet tooling a deployment exists when none
does.
The image builds and was verified locally in `canned-prompts`
(`CANP-WP-0006-T06`): it starts non-root, all six service-level smoke checks
pass against the running container, and the reference CLI installs a package
from it over HTTP.
What remains is publication to `forgejo.coulomb.social/coulomb/canned-prompts`,
which needs registry credentials. Then replace `pending-publication` in
`declarations/rapp.yaml`, `manifests/runtime.yaml` and `manifests/migration.yaml`
with the `@sha256:` digest — the same digest in all three, since
`live-image-digest-match` compares them.
## Provision database roles and credentials
```task
id: RCP-WP-0002-T02
status: todo
priority: high
state_hub_task_id: "61f5cbd7-8fcf-5156-87da-57239ae55d8f"
```
Two roles, deliberately separate: a runtime role that reads and writes rows, and
a migration role that owns the schema. A service able to `ALTER` its own tables
at runtime turns any code defect into a schema defect.
Needed from `rapp-postgres` and the credential broker:
- database `canned_prompts` on `platform-pg-2`;
- `creds/canned-prompts-runtime` and `creds/canned-prompts-migration` in OpenBao
under the `database` path;
- the `openbao-canned-prompts-eso-token` secret in `external-secrets`.
`creds/canned-prompts-publish` is **optional**. Without it the service is
read-only, which is the correct posture until per-publisher identity exists —
not a misconfiguration to be worked around.
## Apply and migrate
```task
id: RCP-WP-0002-T03
status: todo
priority: high
state_hub_task_id: "0b8d206e-bd28-5950-abf7-d824015c09a4"
```
Order matters, and the ordering is the point rather than a convenience:
1. `manifests/00-namespace.yaml` — the `railiance.io/postgres-client` label is
what lets the database namespace accept traffic;
2. `manifests/database-secrets.yaml`, then wait for the secrets to materialize;
3. `manifests/migration.yaml` — the Job runs `alembic upgrade head` under the
migration role and must complete before any replica serves;
4. `manifests/runtime.yaml`.
The runtime deliberately does not migrate at start-up. Migrations as a Job keep
a schema rollback separate from a code rollback and stop replicas racing each
other.
## Verify and record evidence
```task
id: RCP-WP-0002-T04
status: todo
priority: high
state_hub_task_id: "6d9eb97c-57e2-5b74-b6eb-455076713417"
```
Run `tools/smoke.sh`. It checks what only the cluster can answer — secrets
materialized, Service is ClusterIP with no Ingress, NetworkPolicies present,
live image digest matches the pin — and calls `canned-prompts`'
`service/tools/smoke.py` for health and migration head rather than holding a
second opinion about whether the service is healthy.
Record the output as evidence, then move `readiness_state` to `deployed`, and to
`verified` only with that evidence attached.
## Decide per-publisher identity
```task
id: RCP-WP-0002-T05
status: wait
priority: medium
state_hub_task_id: "1252f3a1-12ce-5bc1-b003-3e8999d03459"
```
Blocked on a decision in `canned-prompts`, recorded here because it is the
thing that decides what this deployment is *for*.
Today the service authenticates a single shared bearer token proving "the
operator". That is adequate for a private in-cluster registry and inadequate for
the collaborative prompting platform the operator described: every token holder
is indistinguishable, so § 20.1 namespace ownership can be enforced against
anonymous callers but not attributed among publishers.
This repo must not paper over that with cluster configuration implying finer
control than exists. When identity lands upstream, revisit the publish-token
secret and the NetworkPolicy ingress rule, which currently admits any namespace.