Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## Unreleased maintenance

- Reject ZIP member names that alias during Windows/macOS extraction, including trailing dots/spaces, reserved device names and Unicode normalization collisions; apply the same conservative alias check to output/source boundaries.
- Refuse private environment files, keys, databases, logs and generated dependencies/caches in delivery ZIPs, including ignored local files; preserve the previous verified archive on failure.
- Refuse archive output paths that would replace a bundled document or installer.
- Preserve input validation, row-count guards, audit insertion and cache invalidation in all 20 domain transition graphs, matching their JSON RPC contracts.
- Fix unreadable transition audit rows by declaring status payloads as text; keep assignment details as nullable JSON metadata rather than inserting unencoded step strings.
- Require an explicit nullable assignment deadline and verify failed assignments/transitions leave no orphan workflow or audit rows.
- Fix Git Bash drive-path hangs, reject trailing-slash link destinations, preserve late targets, and treat PowerShell bracket paths literally.
- Reject Windows junctions and other reparse points in bundle sources and source parents before reading files outside the catalog.
- Validate manifest paths and SHA-256 records before hashing; reject drive paths, alternate streams, traversal, generated secrets and linked source parents.
- Clarify shared operator access and assignment metadata, without claiming per-owner isolation or dual-control approval.

These changes do not replace the frozen v0.5.2 release assets. Compile the updated graphs with the matching maintenance Editor, whose compiler preserves row-count guards and invalidation arrays.

Existing deployments that created the generated audit `payload` as a native PostgreSQL JSON column need a reviewed migration to text before adopting the updated configuration. Editing a resource declaration does not alter existing database column types.

## 0.5.2 — 2026-10-11

- Add TaskBoard atomic batches and a permission-preserving read-only cursor feed with nullable due dates.
Expand Down
2 changes: 1 addition & 1 deletion EXAMPLE_APPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The catalog contains 25 named projects. The original five demonstrate specialize
| `ResearchVault` | Dataset stewardship, access policies and provenance |
| `HiringPipeline` | Candidate workflow, recruiter ownership and audit events |
| `IncidentCommand` | Incident response, runbooks and realtime command updates |
| `FinanceOps` | Synthetic dual-control batch operations and totals |
| `FinanceOps` | Synthetic batch operations, scoped operators and totals |
| `EditorPluginRegistry` | Forge-backed Editor plugin metadata, permissions and SHA-256 review state |

Except for the explicitly public `PublicCatalog`, each project uses a generated `<SLUG>_BOOTSTRAP_ADMIN_KEY`. Run `forge init`; do not hand-author shared credentials.
Expand Down
182 changes: 92 additions & 90 deletions MANIFEST.sha256

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ The branch workflow checks out `main` separately, installs its real runtime, cop

`scripts/build-example-bundle.py` produces a bounded, symlink-free, byte-for-byte deterministic ZIP containing `app/`, the catalog and README, legal/provenance documents, the two copy installers and an internal `SHA256SUMS`. GitHub Actions uploads the ZIP and its external checksum; it never publishes a Release automatically.

Packaging refuses local environment files, keys/certificates, databases and their journals, logs, caches, dependency directories and build output inside examples, even when Git ignores them. Remove local runtime state from the source checkout before packaging; an existing archive stays intact on failure. Public `.env.example` templates and project-owned JSON seed files remain supported.

Delivery filenames must be portable across Windows, macOS and Linux: trailing dots/spaces, Windows device names and forbidden characters are refused, along with case or Unicode normalization collisions. ZIP output paths must also remain separate from source files and `app/` under these alias rules, even on a case-sensitive build host.

### Contribute catalog changes

Update `scripts/generate_example_catalog.py` for the 20 generated apps, then run:
Expand Down
4 changes: 3 additions & 1 deletion app/CivicPortal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Public-service back-office workflow with private mutations, case ownership, poli

| Area | Included behavior |
|---|---|
| Main records | `civic/service_requests` CRUD with bounded inputs, soft deletion and ownership policy |
| Main records | `civic/service_requests` CRUD with bounded inputs, soft deletion and assignment metadata |
| Atomic imports | Up to 100 primary records per transaction, with per-row defaults |
| Supporting records | Policy, assignment and audit resources |
| Workflow | Three transactional/analytics RPCs; states: `submitted`, `accepted`, `in_progress`, `resolved`, `rejected` |
Expand Down Expand Up @@ -43,6 +43,8 @@ Use `POST /api/civic-portal/v1/civic/service_requests/_batch` for up to 100 reco

Read `app.json` for the API prefix, `config/40-resources.json` for allowed CRUD fields/actions, `config/50-operations.json` for SQL/RPC contracts, and `config/60-events.json` for channel permissions. Keep changes to JSON configuration validated with the main runtime.

`civic.assign` requires `record_id`, `assigned_to`, `step` and `due_at`; send `due_at: null` when no deadline is needed. Assignment details remain nullable JSON metadata. Transition audit `payload` stores the new status as text. Role permissions apply across this shared back-office project; the `owner_id` field is assignment metadata and does not enforce per-owner isolation.

This README and the numbered fragments are generated by `scripts/generate_example_catalog.py` in exampleApps. Change that generator when contributing catalog updates, then run it and its `--check` mode. A private deployment can adapt its own copied configuration under the project license.

## Deployment boundaries and license
Expand Down
2 changes: 1 addition & 1 deletion app/CivicPortal/config/40-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@
"max_length": 96
},
"payload": {
"type": "json"
"type": "text"
},
"idempotency_key": {
"type": "string",
Expand Down
8 changes: 4 additions & 4 deletions app/CivicPortal/config/50-operations.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
"required": [
"record_id",
"assigned_to",
"step"
"step",
"due_at"
],
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -139,14 +140,13 @@
},
"statements": [
{
"sql": "INSERT INTO civic_workflows (record_id, step, state, assigned_to, due_at, details, created_at) VALUES (:record_id, :step, 'open', :assigned_to, :due_at, :details, CURRENT_TIMESTAMP)",
"sql": "INSERT INTO civic_workflows (record_id, step, state, assigned_to, due_at, created_at) VALUES (:record_id, :step, 'open', :assigned_to, :due_at, CURRENT_TIMESTAMP)",
"mode": "execute",
"params": {
"record_id": "$body.record_id",
"step": "$body.step",
"assigned_to": "$body.assigned_to",
"due_at": "$body.due_at",
"details": "$body.step"
"due_at": "$body.due_at"
},
"result_name": "assignment"
},
Expand Down
83 changes: 76 additions & 7 deletions app/CivicPortal/graphs/domain-transition.forgegraph.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,41 @@
"x": 0,
"y": 0,
"properties": {
"method": "POST"
"method": "POST",
"input_schema": {
"type": "object",
"required": [
"record_id",
"next_status",
"actor_id"
],
"additionalProperties": false,
"properties": {
"record_id": {
"type": "integer",
"minimum": 1
},
"next_status": {
"type": "string",
"enum": [
"submitted",
"accepted",
"in_progress",
"resolved",
"rejected"
]
},
"actor_id": {
"type": "string",
"minLength": 1,
"maxLength": 96
},
"reason": {
"type": "string",
"maxLength": 500
}
}
}
}
},
{
Expand All @@ -31,38 +65,66 @@
{
"id": "update",
"type": "data.mutate",
"title": "Update state",
"title": "Update active record",
"x": 600,
"y": 0,
"properties": {
"sql": "UPDATE civic_service_requests SET status = :next_status WHERE id = :record_id",
"sql": "UPDATE civic_service_requests SET status = :next_status, updated_at = CURRENT_TIMESTAMP WHERE id = :record_id AND deleted_at IS NULL",
"mode": "execute",
"params": {
"next_status": "$body.next_status",
"record_id": "$body.record_id"
},
"require_rowcount_min": 1,
"require_rowcount_max": 1,
"result_name": "transition"
}
},
{
"id": "audit",
"type": "data.mutate",
"title": "Record audit event",
"x": 900,
"y": 0,
"properties": {
"sql": "INSERT INTO civic_audit_events (record_id, event_type, actor_id, payload, idempotency_key, created_at) VALUES (:record_id, :event_type, :actor_id, :payload, :idempotency_key, CURRENT_TIMESTAMP)",
"mode": "execute",
"params": {
"record_id": "$body.record_id",
"event_type": "status.transitioned",
"actor_id": "$body.actor_id",
"payload": "$body.next_status",
"idempotency_key": "$header.Idempotency-Key"
},
"result_name": "audit"
}
},
{
"id": "operation",
"type": "operation.call",
"title": "Forge operation",
"x": 900,
"x": 1200,
"y": 0,
"properties": {
"name": "civic.transition",
"method": "POST",
"database": "primary",
"idempotency": true,
"summary": "Graph model for the audited transition"
"summary": "Atomic, idempotent state transition with audit insertion",
"invalidate_resources": [
"civic/service_requests",
"civic/audit-events"
],
"invalidate_operations": [
"civic.dashboard"
]
}
},
{
"id": "response",
"type": "response.output",
"title": "Operation result",
"x": 1200,
"x": 1500,
"y": 0,
"properties": {
"status_code": 200
Expand All @@ -88,11 +150,18 @@
"id": "edge-3",
"from_node": "update",
"from_port": "exec",
"to_node": "operation",
"to_node": "audit",
"to_port": "exec"
},
{
"id": "edge-4",
"from_node": "audit",
"from_port": "exec",
"to_node": "operation",
"to_port": "exec"
},
{
"id": "edge-5",
"from_node": "operation",
"from_port": "exec",
"to_node": "response",
Expand Down
4 changes: 3 additions & 1 deletion app/ClinicFlow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Synthetic care-operations workflow with strict permissions, assignment queues an

| Area | Included behavior |
|---|---|
| Main records | `clinic/care_cases` CRUD with bounded inputs, soft deletion and ownership policy |
| Main records | `clinic/care_cases` CRUD with bounded inputs, soft deletion and assignment metadata |
| Atomic imports | Up to 100 primary records per transaction, with per-row defaults |
| Supporting records | Policy, assignment and audit resources |
| Workflow | Three transactional/analytics RPCs; states: `intake`, `triage`, `scheduled`, `in_care`, `closed` |
Expand Down Expand Up @@ -43,6 +43,8 @@ Use `POST /api/clinic-flow/v1/clinic/care_cases/_batch` for up to 100 records in

Read `app.json` for the API prefix, `config/40-resources.json` for allowed CRUD fields/actions, `config/50-operations.json` for SQL/RPC contracts, and `config/60-events.json` for channel permissions. Keep changes to JSON configuration validated with the main runtime.

`clinic.assign` requires `record_id`, `assigned_to`, `step` and `due_at`; send `due_at: null` when no deadline is needed. Assignment details remain nullable JSON metadata. Transition audit `payload` stores the new status as text. Role permissions apply across this shared back-office project; the `owner_id` field is assignment metadata and does not enforce per-owner isolation.

This README and the numbered fragments are generated by `scripts/generate_example_catalog.py` in exampleApps. Change that generator when contributing catalog updates, then run it and its `--check` mode. A private deployment can adapt its own copied configuration under the project license.

## Deployment boundaries and license
Expand Down
2 changes: 1 addition & 1 deletion app/ClinicFlow/config/40-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@
"max_length": 96
},
"payload": {
"type": "json"
"type": "text"
},
"idempotency_key": {
"type": "string",
Expand Down
8 changes: 4 additions & 4 deletions app/ClinicFlow/config/50-operations.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
"required": [
"record_id",
"assigned_to",
"step"
"step",
"due_at"
],
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -139,14 +140,13 @@
},
"statements": [
{
"sql": "INSERT INTO clinic_workflows (record_id, step, state, assigned_to, due_at, details, created_at) VALUES (:record_id, :step, 'open', :assigned_to, :due_at, :details, CURRENT_TIMESTAMP)",
"sql": "INSERT INTO clinic_workflows (record_id, step, state, assigned_to, due_at, created_at) VALUES (:record_id, :step, 'open', :assigned_to, :due_at, CURRENT_TIMESTAMP)",
"mode": "execute",
"params": {
"record_id": "$body.record_id",
"step": "$body.step",
"assigned_to": "$body.assigned_to",
"due_at": "$body.due_at",
"details": "$body.step"
"due_at": "$body.due_at"
},
"result_name": "assignment"
},
Expand Down
Loading