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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ jobs:
- name: Verify exact release runtime
if: matrix.node == '24.18.1'
run: corepack pnpm@10.34.0 verify
# Drives the released daemon, installed from npm at the locked integrity,
# through the built client: commands, adoption replay, and event resume
# across a daemon restart. Needs the build that the verify step produced.
- name: Drive the released Coven v0.4.7 daemon through the SDK
if: matrix.node == '24.18.1'
env:
COVEN_CLI_DIR: ${{ runner.temp }}/coven-cli-0.4.7
run: |
set -euo pipefail
mkdir -p "$COVEN_CLI_DIR"
cp conformance/automations-v1-daemon/package.json \
conformance/automations-v1-daemon/package-lock.json "$COVEN_CLI_DIR/"
npm ci --prefix "$COVEN_CLI_DIR" --ignore-scripts --no-audit --no-fund
npm audit signatures --prefix "$COVEN_CLI_DIR"
corepack pnpm@10.34.0 canary:automations-v1-daemon -- \
--coven "$COVEN_CLI_DIR/node_modules/@opencoven/cli/bin/coven.js" \
--expect-version 0.4.7
# The moving major validates supported package behavior without claiming
# exact release, conformance, provenance, or environment authority.
- name: Verify Node 24 package compatibility
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,64 @@ tarball remains an external immutable artifact rather than a committed binary:
corepack pnpm@10.34.0 verify:automations-v1-evidence
```

## Automations v1 daemon canary

The artifact canary checks the contract a release ships. The daemon canary
checks that the released daemon behaves that way when this SDK's built client
drives it. CI installs `@opencoven/cli` from npm at the version and integrity
locked in
[`conformance/automations-v1-daemon/package-lock.json`](conformance/automations-v1-daemon/package-lock.json),
runs `npm audit signatures` over it, and then runs:

```bash
corepack pnpm@10.34.0 build
corepack pnpm@10.34.0 canary:automations-v1-daemon -- \
--coven /path/to/node_modules/@opencoven/cli/bin/coven.js \
--expect-version 0.4.7
```

The canary refuses any binary that does not report the expected version. It
starts `coven daemon serve` in an owned temporary `COVEN_HOME`, with a minimal
environment and no harness, and discovers it the way a consumer would. Then it:

- creates a draft and requires the daemon's stored `integrity` to equal
`computeDefinitionDigest()`, then resends it under the same adoption key
(`replayed`) and with a changed body (`ADOPTION_REPLAY_MISMATCH`);
- revises it, has a stale revision refused with `REVISION_CONFLICT` and the
current revision, activates and pauses it, and reads it back with `get()`;
- stops the daemon with `SIGTERM`, requires it to remove its socket and
`daemon.json`, starts it again over the same home, and requires the earlier
activation to come back `replayed` under its adoption key;
- disables the routine, resumes `subscribe()` from a checkpoint taken before the
restart, and requires exactly the four later lifecycle events followed by the
final empty page and its checkpoint, then the same tail from a concrete
`after` cursor;
- requires empty occurrence and run history. The schedule is set twelve hours
away from the activation, so nothing fires.

It prints one line, for example
`Automations v1 daemon verified: covenVersion=0.4.7 daemonStarts=2 commands=9 … peerIdentity=harness-asserted`.
A failure prints the daemon's own output. On success, failure, `SIGINT`, or
`SIGTERM` (exit 130 or 143), the daemon is stopped and its home removed. Unix
only.

What it does not establish:

- **Peer identity is asserted, not inspected.** Node has no peer-credential API.
The canary launched the daemon under its own uid in a `0700` home, and checks
that the home and socket belong to that uid before asserting it. Production
callers still need a reviewed provider.
- **No run executes.** Runs, receipts, and runtime authority are out of scope.
- **`get()` is v0.4.7's legacy routine projection**, not the stored rich
definition.
- **`eventDefinitionDigest=differs-from-definition-integrity`** records that
v0.4.7's lifecycle events carry the digest of that routine projection, not
the definition document's `integrity`. Coven's occurrences, runs, and receipts
pin the same projection digest, so passing a definition's `integrity` to
`verifyReceipt()` as the expected `definitionDigest` will not match a v0.4.7
receipt. [Coven #1054](https://github.com/OpenCoven/coven/issues/1054) tracks
this.

## Choosing a package

| Need | Package |
Expand Down
84 changes: 84 additions & 0 deletions conformance/automations-v1-daemon/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions conformance/automations-v1-daemon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "opencoven-automations-v1-daemon-canary",
"private": true,
"description": "Pins the released Coven CLI that scripts/verify-automations-v1-daemon.mjs drives.",
"license": "MIT",
"dependencies": {
"@opencoven/cli": "0.4.7"
}
}
7 changes: 7 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ authentication remains explicitly unperformed.
lock and the exact-runtime CI reproduction from the pre-release `8a796807`
artifact to the producer of the released Coven v0.4.7 bundle: `c93a8a93`,
19 files, contract content `ef266d16`.
[SDK #338](https://github.com/OpenCoven/sdk/pull/338) adds a daemon canary.
CI installs the published `@opencoven/cli@0.4.7` at its locked integrity and
drives its daemon through the built client: draft, revise and lifecycle
commands, adoption replay and refusal, and checkpoint resume across a daemon
restart. It found that v0.4.7's lifecycle events, occurrences, runs and
receipts pin the digest of the legacy routine projection rather than the
definition document's `integrity` (reported on Coven #1054).

OpenCoven/coven#991 (`d277ade3`) and OpenCoven/coven#999 (`735e2f05`) publish packaged base capability
negotiation, durable `CAPABILITY_UNSUPPORTED` outcomes, and exact wire request
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"api:baseline:update": "node ./scripts/update-api-baselines.mjs",
"build": "corepack pnpm@10.34.0 --recursive --filter './packages/*' build && corepack pnpm@10.34.0 --recursive --filter './examples/*' build",
"canary:automations-v1": "node ./scripts/verify-automations-v1-artifact.mjs",
"canary:automations-v1-daemon": "node ./scripts/verify-automations-v1-daemon.mjs",
"clean:public-dist": "node ./scripts/clean-public-package-dist.mjs",
"changeset": "changeset",
"cleanup:merged": "node ./scripts/cleanup-merged-branch.mjs",
Expand Down
67 changes: 67 additions & 0 deletions scripts/verify-automations-v1-daemon.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export interface DaemonCanaryArguments {
coven: string;
expectVersion: string;
}

export interface DaemonCanaryCommandContext {
adoptionKey: string;
intent: string;
principalId: string;
}

export interface DaemonCanaryEventStream {
kind: 'automation' | 'occurrence' | 'run';
id: string;
}

export type DaemonCanaryEventQuery =
| { stream: DaemonCanaryEventStream; after?: number }
| { stream: DaemonCanaryEventStream; checkpoint: string };

/** The slice of `CovenAutomationsClient` the scenario drives. */
export interface DaemonCanaryClient {
capabilities(): Promise<unknown>;
createDraft(definition: Record<string, unknown>, context: DaemonCanaryCommandContext): Promise<unknown>;
revise(
automationId: string,
expectedRevision: number,
definition: Record<string, unknown>,
context: DaemonCanaryCommandContext,
): Promise<unknown>;
activate(automationId: string, expectedRevision: number, context: DaemonCanaryCommandContext): Promise<unknown>;
pause(automationId: string, expectedRevision: number, context: DaemonCanaryCommandContext): Promise<unknown>;
disable(automationId: string, expectedRevision: number, context: DaemonCanaryCommandContext): Promise<unknown>;
get(automationId: string): Promise<unknown>;
list(): Promise<unknown>;
events(query: DaemonCanaryEventQuery): Promise<unknown>;
subscribe(query: DaemonCanaryEventQuery): AsyncIterable<unknown>;
occurrenceHistory(automationId: string, query: { limit: number }): Promise<unknown>;
runHistory(automationId: string, query: { limit: number }): Promise<unknown>;
}

export interface DaemonCanarySummary {
commands: number;
replays: number;
rejections: number;
events: number;
subscribePages: number;
eventDefinitionDigest: 'matches-definition-integrity' | 'differs-from-definition-integrity';
}

export function parseDaemonCanaryArguments(argv: string[]): DaemonCanaryArguments;
export function quietScheduleHour(now?: Date): number;
export function canaryDefinition(scheduleHour: number): Record<string, unknown>;
export function runDaemonScenario(options: {
sdk: { computeDefinitionDigest(definition: unknown): unknown };
connect: () => Promise<DaemonCanaryClient>;
restartDaemon: () => Promise<void>;
scheduleHour: number;
}): Promise<DaemonCanarySummary>;
export function verifyCovenVersion(coven: string, expectVersion: string, home: string): string;
export function harnessAssertedSecurity(
discovered: unknown,
home: string,
): { platform: 'unix'; peerIdentity: { inspectConnected(socket: unknown): Promise<{ uid: number }> } };
export function verifyAutomationsDaemon(
options: DaemonCanaryArguments & { signal?: AbortSignal },
): Promise<DaemonCanarySummary & { covenVersion: string; daemonStarts: number }>;
Loading
Loading