diff --git a/cli/conformance/cases/service/framework/service-guide-human.json b/cli/conformance/cases/service/framework/service-guide-human.json index b01ee3cb..7fb1ab2d 100644 --- a/cli/conformance/cases/service/framework/service-guide-human.json +++ b/cli/conformance/cases/service/framework/service-guide-human.json @@ -13,7 +13,7 @@ }, "exitCode": 0, "stdout": { - "text": "# OpenProse service guide\n\n## Start here\n\n`prose cli` commands reach the hosted OpenProse service. They never\nprompt and never run anything locally. This guide is the workflow view;\nthe per-command facts come from these commands:\n\n- `prose cli service triage --json`: where this session stands, in one call:\n health, whether the key works (or the variable to set), balance, default\n organization, recent runs, jobs and `nextCommands`. Each section has its\n own `problem`; it exits 0 whenever it produces the report.\n- `prose cli service capabilities --json`: grammar, exit code of every error\n code, environment variables and every command with examples, on one line.\n- `prose cli service operations --json`: every command with its arguments,\n options and effect, as JSON.\n- `prose cli --help`: one command, ending with its exit codes.\n- `prose cli service guide --json`: this guide as\n `{\"sections\":[{\"id\",\"title\",\"body\"}]}` in the usual envelope.\n\nProgram files (`.prose.md`) are not described here: read a working one with\n`prose cli example list` and `prose cli example show NAME`. New to the\nservice? Start with \"Your first program\" below, then \"Run it every day\",\n\"Scripting\", \"What will it cost\", \"Headless and CI keys\" and \"Sharing\".\n\n## Your first program\n\nStart from a working example, run it once, then save it.\n\n```sh\nprose cli example list\nprose cli example show NAME --output-file hello.prose.md\nprose cli run submit hello.prose.md --preview\nprose cli run submit hello.prose.md --yes\nprose cli program save hello hello.prose.md\n```\n\n1. `cli example list` prints the example names. `cli example show` with\n `--output-file` writes one to a new file; edit it with any editor.\n2. `--preview` shows what the run would do and what it would reserve, and\n sends nothing. `--yes` starts the paid run and streams its answer.\n3. `cli program save SLUG FILE` keeps it as your own private program. Run\n the saved program with `prose cli run submit --from hello --yes`.\n\nTools a run can use depend on its environment (`--environment`): `builtin`\nhas file tools only; `linux` adds a shell, without network access. A\nprogram reaches the web through the `browser:interact` tool.\n\n## Run it every day\n\nA job runs a saved program on its own. A schedule job repeats every\n`interval_seconds` (60 to 2678400; 86400 is once a day). The first run\nstarts about one second after the job is created. There is no cron\nexpression or time of day yet.\n\n```sh\nprose cli program save hello hello.prose.md --json\nprose cli program show hello --json\nprintf '%s' '{\"type\":\"schedule\",\"program_ref\":\"OWNER/hello@REV\",\"interval_seconds\":86400}' > daily.json\nprose cli job create --spec-file daily.json --preview\nprose cli job create --spec-file daily.json --yes --json\nprose cli job list\nprose cli job delete JOB_ID --yes\n```\n\n`program_ref` is the pinned reference `result.program.ref` that\n`program save` and `program show` print (`OWNER/SLUG@REV`). Each run is paid\nlike any other. `cli job list` shows the next run time; `cli job delete`\nstops the schedule for good.\n\nTo run at a fixed time of day instead, let the operating system's scheduler\n(cron, launchd) run one command with `OPENPROSE_API_KEY` set:\n\n```sh\nprose cli run submit --from OWNER/hello --yes --json | jq .result.run.response\n```\n\n## Scripting\n\n- Pass `--json` and read `result`; on an error, `problem.code` and\n `problem.details`. Every exit code is in the table in \"Exit codes, resume\n and detach\"; a script branches on it with `case $?`.\n- The run id is `result.runId` (`result.run.run_id` once the run ends).\n- For a long run, submit with `--detach` (exit 0 as soon as the run id is\n known), then follow it with `prose cli run watch RUN_ID --wait 10m --json`.\n If `--wait` passes first, the exit is 21 and `problem.details.resumeArgv`\n is the command to run next. Never submit again to resume.\n- `cli run list` and `cli wallet events` return one page. Pass\n `result.nextBefore` to `--before` for the next one; it is `null` on the\n last page.\n\n```sh\nprose cli run submit --from OWNER/hello --yes --detach --json > run.json\nprose cli run watch RUN_ID --wait 10m --json\nprose cli run list --limit 50 --before CURSOR --json\n```\n\n## What will it cost\n\nA run reserves a hold before it starts: money set aside from the wallet\n(currently $1.02), not the price. The price is known only after the run\nsettles: `price_cents` in `prose cli run show RUN_ID --json`, of which\n`environment_price_cents` is the environment's share. What the run did not\nuse is released. Short runs usually cost a few cents.\n\nEstimate from your own history: `prose cli wallet usage` prints runs and\nprices by day, and `prose cli wallet balance` shows what is available and\nwhat is reserved right now. `prose cli run quote --json` reports the hold.\n\nPremium models unlock with any wallet top-up; `prose cli model list` shows\neach model's status.\n\n## Headless and CI keys\n\nA machine without a person at a browser uses an API key in\n`OPENPROSE_API_KEY`; it wins over the stored key. Keep it in the CI system's\nsecret store and set it only for the commands that need it.\n`prose cli auth login` stores its key in the operating system credential\nstore, which a launchd or cron job cannot reach without a login session:\nset `OPENPROSE_API_KEY` in the job instead. Check a key with\n`prose cli auth status --json`.\n\n## Sharing\n\n`prose cli run share RUN_ID --yes` creates a public link to a run's outputs.\nAnyone with the link can open it for 24 hours, and it cannot be revoked. To\ngive specific people access, invite them to your organization instead:\n`prose cli org invite --help`.\n\n## Grammar\n\n```text\nprose [--output human|json|jsonl] cli [ARGUMENTS] [OPTIONS]\n```\n\n- Every service command starts with `cli`. Without it (`run submit` or\n `wallet balance` right after `prose`) the command exits 2 and\n `details.suggestedArgv` names the `cli` command.\n- Global options go before `cli`; command options go after the command path.\n `--output` may also follow the command path.\n A command option before `cli` (`--json`, `--yes`, `--limit 5`) exits 2 and\n `details.suggestedArgv` moves it after the command path.\n- `--json` is `--output json`: one JSON object on one line. Streaming\n commands (`cli run submit`, `cli run watch`) print one event per line with\n `--output jsonl` and end with exactly one `service.completed`,\n `service.failed` or `service.detached` line (`service.detached` is exit 21:\n the run continues). List commands (`cli run list`, `cli job list`,\n `cli model list`, ...) print one `openprose.service-record/1` line per item\n with `--output jsonl`, then one `openprose.service-page/1` line with\n `count`, `nextBefore` and the rest of the result in `meta`. That last line\n carries `exitCode`, the process exit code. Every JSON line has its keys\n sorted, so the same response prints the same bytes.\n- Times the service sends as epoch milliseconds (`createdAt`, `nextFireAt`,\n `updated_at`, ...) also appear as `_iso` (RFC 3339 UTC). Human output\n shows money in dollars and ends with `Next:` commands you can copy.\n- Nothing is guessed. A misspelled or foreign word exits 2\n (`INVOCATION_INVALID`); `details.suggestedArgv` is the corrected argv (the\n words after `prose`). Rerun it; it keeps your output mode. A few exact\n spellings are aliases and run their command: `cli run status` and\n `cli run get` are `cli run show`, `cli run logs`, `cli run tail` and\n `cli run wait` are `cli run watch`, `cli run create` and `cli run start` are\n `cli run submit`, `cli program push` and `cli program upload` are\n `cli program save`, and `cli program rm` and `cli job rm` are `delete` (still\n with `--yes`). The manifest lists them in `grammar.intentInference.commandAliases`.\n A suggestion is a complete command that parses: a group gains its verb\n (`cli model list`), a dollar amount becomes cents (`--amount-cents 500`),\n and an out-of-range `--limit` becomes the nearest accepted value. When the\n fix reads standard input, `details.suggestedStdin` holds what to pipe in;\n a secret (a credit code, an invitation token) is never echoed, so pipe\n your own.\n\n## Credentials\n\nThe key, first match wins:\n\n1. `OPENPROSE_API_KEY`, when set and non-empty;\n2. the key `prose cli auth login` stored in the operating-system credential\n store (login needs a person at a browser).\n\nA variable always wins over the stored key, so a bad variable must be fixed\nor unset. `prose cli auth status --json` reports `credentialSource`.\n`SERVICE_AUTH_REQUIRED` names `details.credentialVariable` and\n`details.credentialProblem` (`missing`, `malformed` or `rejected`), and its\nAction follows where the key came from: an environment key is replaced or\nunset, never fixed by `cli auth login`. Nothing ever falls back to another\nkey. The output mode is `--json` or `--output`, then `PROSE_OUTPUT`, then\nhuman.\n\n## Confirm and preview\n\nA command that spends money, publishes, deletes or cannot be undone needs\n`--yes`. Without it nothing is changed: it exits 2 with `CONFIRMATION_REQUIRED`,\n`details.reason` (what `--yes` would do, for example that rotating a secret\ninvalidates the old one now), `details.plannedRequest` (the method, what the\nrequest does as `description`, its non-secret inputs as `summary`: model, programRef, inputKeys,\namount_cents, slug; and for `cli run submit`, `cli program draft` and a paid\n`cli job create` the service's flat hold quote), `details.confirmArgv` and\n`details.previewArgv`. `--preview` prints the same plan, exits 0 and changes\nnothing. An unknown `--model`, an `--environment` or `--runtime` that\n`prose cli service status --json` does not list (`environments`), an input the\nprogram's `Parameters` section does not declare (or a declared one that is\nmissing and not marked optional), an invalid organization slug or a job spec\nthe service would reject fails here, before any confirmation, with every\nproblem listed:\n\n```sh\nprose cli run submit hello.prose.md --preview\nprose cli run submit hello.prose.md --yes\n```\n\n`prose cli service operations` shows which commands need `--yes` (CONFIRM).\n\n## Exit codes, resume and detach\n\n| Exit | Meaning | What a script does |\n| --- | --- | --- |\n| 0 | success | read `result` |\n| 2 | `INVOCATION_INVALID` or `CONFIRMATION_REQUIRED`; nothing was sent | rerun `details.suggestedArgv`, or add `--yes` on purpose |\n| 10 | service or credential error | read `problem.code` and `details.reason`; retry only when `problem.retryable` is true |\n| 20 | this installation cannot run the command | reinstall the CLI; retrying will not help |\n| 21 | detached: the run continues | run `details.resumeArgv`; never submit again |\n| 22 | the run failed, or the submission is ambiguous | read `details.reason`; if ambiguous, rerun `details.resumeArgv` (it reuses the same session, never a new run) |\n| 24 | cancelled | stop; nothing to resume |\n\nIn JSON mode the error is `problem` (`code`, `retryable`, `action`,\n`details`), with `result` null. This holds for every `cli` command line,\nincluding a near miss or a misplaced flag: then `operation` is `cli` if the\nargv names no operation. Only the language and the commands for this machine\n(listed by the top-level help) print a bare runner error. The exit of every\ncode is in\n`prose cli service capabilities --json` (`exitCodes`).\n\nExit 21 means the CLI stopped following a run that keeps running:\n`--wait` passed (`SERVICE_WATCH_DEADLINE`) or Ctrl-C or a lost stream\n(`HOSTED_RUN_DETACHED`); both are not retryable and carry\n`details.resumable: true`. A `--wait` that passes before the run id arrives\nkeeps reading until the id is known, so it is still exit 21 with the run\nnamed. A successful `--detach` exits 0 with `result.detached`,\n`result.resumeArgv`, `result.cancelArgv` and `result.run` as\n`{run_id, status}`. Results carry the run id as `result.run_id` and\n`result.runId`.\nResume with `resumeArgv` (a `cli run watch` command with `--session`). Never resume by\nsubmitting again: a new submit is a second paid run. Only\n`prose cli run cancel RUN_ID --yes` cancels; on a run that already ended it\nexits 0 with `result.status` `already_ended`. Steering an ended run with\n`cli run input` is not retryable: read it with `details.suggestedArgv`.\n\n`SERVICE_RESOURCE_NOT_FOUND` (10) is not retryable. `details.resource`\nnames the kind and id, and `details.suggestedArgv` lists the ones that\nexist: run it and retry with an id it prints. A well-formed run id the\nservice does not know (`cli run show`, `cli run watch`) and a `--file` the\nrun does not list are this error. A run id with fewer than 64 hex digits was\ncut off when copied: it exits 2 before any request. `latest` in place of a\nrun id is your newest run (`cli run show latest`). `program delete` and\n`job delete` with `--yes` of something already gone exit 0 with\n`result.alreadyAbsent`.\n\n`RUN_SUBMISSION_AMBIGUOUS` (22) means the connection was lost before the\nrun id was known, and the CLI did not repeat the submission. Rerun\n`details.resumeArgv`: the same `cli run submit` with the same `--session`\nand `--detach`. A session never starts a second run: `run submit --session S`\nof a session that already has a run (in this machine's journal, or as the\nservice reports) returns that run with `result.reused: true` and exit 0.\n`prose cli run list --limit 5 --json` (`details.suggestedArgv`) lists recent\nruns.\n\nA run its owner stopped has `cancelled: true` in `cli run show` and\n`cli run list`, and `cli run watch` of it exits 24.\n\n## Paging\n\n`cli run list` and `cli wallet events` are paged. Pass\n`result.nextBefore` to `--before` for the next page; it is `null` on the last\npage.\nThe cursor is opaque. Human mode prints a `Next page:` line with the full\ncommand, and `--output jsonl` carries `nextBefore` in the page line.\n\n```sh\nprose cli run list --limit 50 --json\nprose cli run list --limit 50 --before CURSOR --json\n```\n\n`cli result list` takes `--limit` only (at most 100) and has no cursor.\n\n## Get a run's answer\n\n- While it runs: human mode prints the program's text on stdout. With\n `--json`, the answer is `result.run.response`; `result.run.files` lists the\n files it wrote.\n- After a detach (runs submitted from this machine): replay the whole stream\n with `prose cli run watch RUN_ID --json` and read `result.run.response`.\n For a run started elsewhere, the same command reports an ended run's\n status and files from its record (`result.source` is `record`, no\n response text).\n- Any run, from anywhere: `prose cli run show RUN_ID --json` gives status,\n files and price but never the response text. `result.run.response` is the\n run's text as written, trailing whitespace included. Read a file with\n `prose cli run show RUN_ID --file outputs/result.json`, or fetch them all\n into `./RUN_ID` with `prose cli run download RUN_ID` (or `--output-dir DIR`).\n- A result someone published (`OWNER/SLUG`) is not a run record:\n `prose cli result show OWNER/SLUG --latest --json`.\n\n## Recipes\n\n### Submit and wait\n\n```sh\nprose cli run quote --json\nprose cli run submit hello.prose.md --input topic=otters --preview\nprose cli run submit hello.prose.md --input topic=otters --yes --json > run.json\njq -r '.result.run.response' run.json\n```\n\n`cli run quote` is the wallet hold a run reserves, not its price; the settled\nprice is `result.run.price_cents`. A saved program runs with\n`--from SLUG` (your own) or `--from OWNER/SLUG@REV`; every program record\n(`program list|show|save|revisions --json`) carries that pinned reference as\n`ref`, and `prose cli program show SLUG@1 --json` resolves a revision number\nof your own program to it. On exit 21, rerun\n`problem.details.resumeArgv`; for a long run submit with `--detach` and follow\nwith `prose cli run watch RUN_ID --wait 10m --json`.\n\n### Publish a result\n\nA published result is a completed run of a saved revision of a public\nprogram that wrote `outputs/result.json`.\n\n```sh\nprose cli program save haiku haiku.prose.md --json\nprose cli run submit --from haiku --yes --json\nprose cli program visibility haiku public --preview\nprose cli result publish haiku --run RUN_ID --yes --json\nprose cli result show haiku --json\n```\n\nMaking a program public exposes its source; the CLI never adds `--yes` to that\nstep for you. Reading published results needs no key; a bare `SLUG` (your\nown program) is resolved with your key first, and without a publication id\n`result show` reads the newest.\n\n### Create a webhook job\n\nA job starts runs on its own. The spec is snake_case JSON; a test-mode webhook\nreceives events and never starts runs.\n\n```sh\nprintf '%s' '{\"type\":\"webhook\",\"name\":\"my-hook\",\"delivery_mode\":\"test\"}' > hook.json\nprose cli job create --spec-file hook.json --preview\nprose cli job create --spec-file hook.json --yes --json\nprose cli job deliveries JOB_ID --json\n```\n\n`result.endpoint` and `result.signing_secret` appear only in the create result\n(and in `cli job rotate-secret`); store them then. `result.endpointUrl` is the\nabsolute URL to configure in the sender; `cli job show` repeats it. `prose cli job create --help`\nlists the schedule and webhook spec keys; `prose cli job list --json` lists the\njob types. The keys each type accepts are the `spec` of `job.create` in\n`prose cli service operations --json`; a spec is checked against it before any\nrequest and every problem is reported at once in `details.violations`. A\nwebhook with no `program_ref` starts no runs, so its plan has effect `write`\nand no hold.\n" + "text": "# OpenProse service guide\n\n## Start here\n\n`prose cli` commands reach the hosted OpenProse service. They never\nprompt and never run anything locally. This guide is the workflow view;\nthe per-command facts come from these commands:\n\n- `prose cli service triage --json`: where this session stands, in one call:\n health, whether the key works (or the variable to set), balance, default\n organization, recent runs, jobs and `nextCommands`. Each section has its\n own `problem`; it exits 0 whenever it produces the report.\n- `prose cli service capabilities --json`: grammar, exit code of every error\n code, environment variables and every command with examples, on one line.\n- `prose cli service operations --json`: every command with its arguments,\n options and effect, as JSON.\n- `prose cli --help`: one command, ending with its exit codes.\n- `prose cli service guide --json`: this guide as\n `{\"sections\":[{\"id\",\"title\",\"body\"}]}` in the usual envelope.\n\nProgram files (`.prose.md`) are not described here: read a working one with\n`prose cli example list` and `prose cli example show NAME`. New to the\nservice? Start with \"Your first program\" below, then \"Run it every day\",\n\"Scripting\", \"What will it cost\", \"Headless and CI keys\" and \"Sharing\".\n\n## Your first program\n\nStart from a working example, run it once, then save it.\n\n```sh\nprose cli example list\nprose cli example show NAME --output-file hello.prose.md\nprose cli run submit hello.prose.md --preview\nprose cli run submit hello.prose.md --yes\nprose cli program save hello hello.prose.md\n```\n\n1. `cli example list` prints the example names. `cli example show` with\n `--output-file` writes one to a new file; edit it with any editor.\n2. `--preview` shows what the run would do and what it would reserve, and\n sends nothing. `--yes` starts the paid run and streams its answer.\n3. `cli program save SLUG FILE` keeps it as your own private program. Run\n the saved program with `prose cli run submit --from hello --yes`.\n\nTools a run can use depend on its environment (`--environment`): `builtin`\nhas file tools only; `linux` adds a shell, without network access. A\nprogram reaches the web through the `browser:interact` tool.\n\n## Run it every day\n\nA job runs a saved program on its own. A schedule job repeats every\n`interval_seconds` (60 to 2678400; 86400 is once a day). The first run\nstarts about one second after the job is created. There is no cron\nexpression or time of day yet.\n\n```sh\nprose cli program save hello hello.prose.md --json\nprose cli program show hello --json\nprintf '%s' '{\"type\":\"schedule\",\"program_ref\":\"OWNER/hello@REV\",\"interval_seconds\":86400}' > daily.json\nprose cli job create --spec-file daily.json --preview\nprose cli job create --spec-file daily.json --yes --json\nprose cli job list\nprose cli job delete JOB_ID --yes\n```\n\n`program_ref` is the pinned reference `result.program.ref` that\n`program save` and `program show` print (`OWNER/SLUG@REV`). Each run is paid\nlike any other. `cli job list` shows the next run time; `cli job delete`\nstops the schedule for good.\n\nTo run at a fixed time of day instead, let the operating system's scheduler\n(cron, launchd) run one command with `OPENPROSE_API_KEY` set:\n\n```sh\nprose cli run submit --from OWNER/hello --yes --json | jq .result.run.response\n```\n\n## Scripting\n\n- Pass `--json` and read `result`; on an error, `problem.code` and\n `problem.details`. Every exit code is in the table in \"Exit codes, resume\n and detach\"; a script branches on it with `case $?`.\n- The run id is `result.runId` (`result.run.run_id` once the run ends).\n- For a long run, submit with `--detach` (exit 0 as soon as the run id is\n known), then follow it with `prose cli run watch RUN_ID --wait 10m --json`.\n If `--wait` passes first, the exit is 21 and `problem.details.resumeArgv`\n is the command to run next. Never submit again to resume.\n- `cli run list` and `cli wallet events` return one page. Pass\n `result.nextBefore` to `--before` for the next one; it is `null` on the\n last page.\n\n```sh\nprose cli run submit --from OWNER/hello --yes --detach --json > run.json\nprose cli run watch RUN_ID --wait 10m --json\nprose cli run list --limit 50 --before CURSOR --json\n```\n\n## What will it cost\n\nA run reserves a hold before it starts: money set aside from the wallet\n(currently $1.02), not the price. The price is known only after the run\nsettles: `price_cents` in `prose cli run show RUN_ID --json`, of which\n`environment_price_cents` is the environment's share. What the run did not\nuse is released. Short runs usually cost a few cents.\n\nEstimate from your own history: `prose cli wallet usage` prints runs and\nprices by day, and `prose cli wallet balance` shows what is available and\nwhat is reserved right now. `prose cli run quote --json` reports the hold.\n\nPremium models unlock with any wallet top-up; `prose cli model list` shows\neach model's status.\n\n## Headless and CI keys\n\nA machine without a person at a browser uses an API key in\n`OPENPROSE_API_KEY`; it wins over the stored key. Keep it in the CI system's\nsecret store and set it only for the commands that need it.\n`prose cli auth login` stores its key in the operating system credential\nstore, which a launchd or cron job cannot reach without a login session:\nset `OPENPROSE_API_KEY` in the job instead. Check a key with\n`prose cli auth status --json`.\n\n## Sharing\n\n`prose cli run share RUN_ID --yes` creates a public link to a run's outputs.\nAnyone with the link can open it for 24 hours, and it cannot be revoked. To\ngive specific people access, invite them to your organization instead:\n`prose cli org invite --help`.\n\n## Grammar\n\n```text\nprose [--output human|json|jsonl] cli [ARGUMENTS] [OPTIONS]\n```\n\n- Every service command starts with `cli`. Without it (`run submit` or\n `wallet balance` right after `prose`) the command exits 2 and\n `details.suggestedArgv` names the `cli` command.\n- Global options go before `cli`; command options go after the command path.\n `--output` may also follow the command path.\n A command option before `cli` (`--json`, `--yes`, `--limit 5`) exits 2 and\n `details.suggestedArgv` moves it after the command path.\n- `--json` is `--output json`: one JSON object on one line. Streaming\n commands (`cli run submit`, `cli run watch`) print one event per line with\n `--output jsonl` and end with exactly one `service.completed`,\n `service.failed` or `service.detached` line (`service.detached` is exit 21:\n the run continues). List commands (`cli run list`, `cli job list`,\n `cli model list`, ...) print one `openprose.service-record/1` line per item\n with `--output jsonl`, then one `openprose.service-page/1` line with\n `count`, `nextBefore` and the rest of the result in `meta`. That last line\n carries `exitCode`, the process exit code. Every JSON line has its keys\n sorted, so the same response prints the same bytes.\n- Times the service sends as epoch milliseconds (`createdAt`, `nextFireAt`,\n `updated_at`, ...) also appear as `_iso` (RFC 3339 UTC). Human output\n shows money in dollars and ends with `Next:` commands you can copy.\n- Nothing is guessed. A misspelled or foreign word exits 2\n (`INVOCATION_INVALID`); `details.suggestedArgv` is the corrected argv (the\n words after `prose`). Rerun it; it keeps your output mode. A few exact\n spellings are aliases and run their command: `cli run status` and\n `cli run get` are `cli run show`, `cli run logs`, `cli run tail` and\n `cli run wait` are `cli run watch`, `cli run create` and `cli run start` are\n `cli run submit`, `cli program push` and `cli program upload` are\n `cli program save`, and `cli program rm` and `cli job rm` are `delete` (still\n with `--yes`). The manifest lists them in `grammar.intentInference.commandAliases`.\n A suggestion is a complete command that parses: a group gains its verb\n (`cli model list`), a dollar amount becomes cents (`--amount-cents 500`),\n and an out-of-range `--limit` becomes the nearest accepted value. When the\n fix reads standard input, `details.suggestedStdin` holds what to pipe in;\n a secret (a credit code, an invitation token) is never echoed, so pipe\n your own.\n\n## Credentials\n\nThe key, first match wins:\n\n1. `OPENPROSE_API_KEY`, when set and non-empty;\n2. the key `prose cli auth login` stored in the operating-system credential\n store (login needs a person at a browser).\n\nA variable always wins over the stored key, so a bad variable must be fixed\nor unset. `prose cli auth status --json` reports `credentialSource`.\n`SERVICE_AUTH_REQUIRED` names `details.credentialVariable` and\n`details.credentialProblem` (`missing`, `malformed` or `rejected`), and its\nAction follows where the key came from: an environment key is replaced or\nunset, never fixed by `cli auth login`. Nothing ever falls back to another\nkey. The output mode is `--json` or `--output`, then `PROSE_OUTPUT`, then\nhuman.\n\n## Confirm and preview\n\nA command that spends money, publishes, deletes or cannot be undone needs\n`--yes`. Without it nothing is changed: it exits 2 with `CONFIRMATION_REQUIRED`,\n`details.reason` (what `--yes` would do, for example that rotating a secret\ninvalidates the old one now), `details.plannedRequest` (the method, what the\nrequest does as `description`, its non-secret inputs as `summary`: model, programRef, inputKeys,\namount_cents, slug; and for `cli run submit`, `cli program draft` and a paid\n`cli job create` the service's flat hold quote), `details.confirmArgv` and\n`details.previewArgv`. `--preview` prints the same plan, exits 0 and changes\nnothing. An unknown `--model`, an `--environment` or `--runtime` that\n`prose cli service status --json` does not list (`environments`), an input the\nprogram's `Parameters` section does not declare (or a declared one that is\nmissing and not marked optional), an invalid organization slug or a job spec\nthe service would reject fails here, before any confirmation, with every\nproblem listed:\n\n```sh\nprose cli run submit hello.prose.md --preview\nprose cli run submit hello.prose.md --yes\n```\n\n`prose cli service operations` shows which commands need `--yes` (CONFIRM).\n\n## Exit codes, resume and detach\n\n| Exit | Meaning | What a script does |\n| --- | --- | --- |\n| 0 | success | read `result` |\n| 2 | `INVOCATION_INVALID` or `CONFIRMATION_REQUIRED`; nothing was sent | rerun `details.suggestedArgv`, or add `--yes` on purpose |\n| 10 | service or credential error | read `problem.code` and `details.reason`; retry only when `problem.retryable` is true |\n| 20 | this installation cannot run the command | reinstall the CLI; retrying will not help |\n| 21 | detached: the run continues | run `details.resumeArgv`; never submit again |\n| 22 | the run failed, or the submission is ambiguous | read `details.reason`; if ambiguous, rerun `details.resumeArgv` (it reuses the same session, never a new run) |\n| 24 | cancelled | stop; nothing to resume |\n\nIn JSON mode the error is `problem` (`code`, `retryable`, `action`,\n`details`), with `result` null. This holds for every `cli` command line,\nincluding a near miss or a misplaced flag: then `operation` is `cli` if the\nargv names no operation. Only the language and the commands for this machine\n(listed by the top-level help) print a bare runner error. The exit of every\ncode is in\n`prose cli service capabilities --json` (`exitCodes`).\n\nExit 21 means the CLI stopped following a run that keeps running:\n`--wait` passed (`SERVICE_WATCH_DEADLINE`) or Ctrl-C or a lost stream\n(`HOSTED_RUN_DETACHED`); both are not retryable and carry\n`details.resumable: true`. A `--wait` that passes before the run id arrives\nkeeps reading until the id is known, so it is still exit 21 with the run\nnamed. A successful `--detach` exits 0 with `result.detached`,\n`result.resumeArgv`, `result.cancelArgv` and `result.run` as\n`{run_id, status}`. Results carry the run id as `result.run_id` and\n`result.runId`.\nResume with `resumeArgv` (a `cli run watch` command with `--session`). Never resume by\nsubmitting again: a new submit is a second paid run. Only\n`prose cli run cancel RUN_ID --yes` cancels; on a run that already ended it\nexits 0 with `result.status` `already_ended`. Steering an ended run with\n`cli run input` is not retryable: read it with `details.suggestedArgv`.\n\n`SERVICE_RESOURCE_NOT_FOUND` (10) is not retryable. `details.resource`\nnames the kind and id, and `details.suggestedArgv` lists the ones that\nexist: run it and retry with an id it prints. A well-formed run id the\nservice does not know (`cli run show`, `cli run watch`) and a `--file` the\nrun does not list are this error. A run id with fewer than 64 hex digits was\ncut off when copied: it exits 2 before any request. `latest` in place of a\nrun id is your newest run (`cli run show latest`). `program delete` and\n`job delete` with `--yes` of something already gone exit 0 with\n`result.alreadyAbsent`.\n\n`RUN_SUBMISSION_AMBIGUOUS` (22) means the connection was lost before the\nrun id was known, and the CLI did not repeat the submission. Rerun\n`details.resumeArgv`: the same `cli run submit` with the same `--session`\nand `--detach`. A session never starts a second run: `run submit --session S`\nof a session that already has a run (in this machine's journal, or as the\nservice reports) returns that run with `result.reused: true` and exit 0.\n`prose cli run list --limit 5 --json` (`details.suggestedArgv`) lists recent\nruns.\n\nA run its owner stopped has `cancelled: true` in `cli run show` and\n`cli run list`, and `cli run watch` of it exits 24.\n\n## Paging\n\n`cli run list` and `cli wallet events` are paged. Pass\n`result.nextBefore` to `--before` for the next page; it is `null` on the last\npage.\nThe cursor is opaque. Human mode prints a `Next page:` line with the full\ncommand, and `--output jsonl` carries `nextBefore` in the page line.\n\n```sh\nprose cli run list --limit 50 --json\nprose cli run list --limit 50 --before CURSOR --json\n```\n\n`cli result list` takes `--limit` only (at most 100) and has no cursor.\n\n## Get a run's answer\n\n- While it runs: human mode prints the program's text on stdout. With\n `--json`, the answer is `result.run.response`; `result.run.files` lists the\n files it wrote.\n- After a detach (runs submitted from this machine): replay the whole stream\n with `prose cli run watch RUN_ID --json` and read `result.run.response`.\n For a run started elsewhere, the same command reports an ended run's\n status and files from its record (`result.source` is `record`, no\n response text).\n- Any run, from anywhere: `prose cli run show RUN_ID --json` gives status,\n files and price but never the response text. `result.run.response` is the\n run's text as written, trailing whitespace included. Read a file with\n `prose cli run show RUN_ID --file outputs/result.json`, or fetch them all\n into `./RUN_ID` with `prose cli run download RUN_ID` (or `--output-dir DIR`).\n- A result someone published (`OWNER/SLUG`) is not a run record:\n `prose cli result show OWNER/SLUG --latest --json`.\n\n## Recipes\n\n### Submit and wait\n\n```sh\nprose cli run quote --json\nprose cli run submit hello.prose.md --input topic=otters --preview\nprose cli run submit hello.prose.md --input topic=otters --yes --json > run.json\njq -r '.result.run.response' run.json\n```\n\n`cli run quote` is the wallet hold a run reserves, not its price; the settled\nprice is `result.run.price_cents`. A saved program runs with\n`--from SLUG` (your own) or `--from OWNER/SLUG@REV`; every program record\n(`program list|show|save|revisions --json`) carries that pinned reference as\n`ref`, and `prose cli program show SLUG@1 --json` resolves a revision number\nof your own program to it. On exit 21, rerun\n`problem.details.resumeArgv`; for a long run submit with `--detach` and follow\nwith `prose cli run watch RUN_ID --wait 10m --json`.\n\n### Publish a result\n\nA published result is a completed run of a saved revision of a public\nprogram that wrote `outputs/result.json`.\n\n```sh\nprose cli program save haiku haiku.prose.md --json\nprose cli run submit --from haiku --yes --json\nprose cli program visibility haiku public --preview\nprose cli result publish haiku --run RUN_ID --yes --json\nprose cli result show haiku --json\n```\n\nMaking a program public exposes its source; the CLI never adds `--yes` to that\nstep for you. Reading published results needs no key; a bare `SLUG` (your\nown program) is resolved with your key first, and without a publication id\n`result show` reads the newest.\n\n### Create a webhook job\n\nA job starts runs on its own. The spec is snake_case JSON; a test-mode webhook\nreceives events and never starts runs.\n\n```sh\nprintf '%s' '{\"type\":\"webhook\",\"name\":\"my-hook\",\"delivery_mode\":\"test\"}' > hook.json\nprose cli job create --spec-file hook.json --preview\nprose cli job create --spec-file hook.json --yes --json\nprose cli job deliveries JOB_ID --json\n```\n\n`result.endpoint` and `result.signing_secret` appear only in the create result\n(and in `cli job rotate-secret`); store them then. `result.endpoint_url` is the\nabsolute URL to configure in the sender; `cli job show` repeats it. `prose cli job create --help`\nlists the schedule and webhook spec keys; `prose cli job list --json` lists the\njob types. The keys each type accepts are the `spec` of `job.create` in\n`prose cli service operations --json`; a spec is checked against it before any\nrequest and every problem is reported at once in `details.violations`. A\nwebhook with no `program_ref` starts no runs, so its plan has effect `write`\nand no hold.\n\nA sender POSTs the event as JSON (`Content-Type: application/json`, at most\n256 KiB) to the endpoint URL. A webhook created without `receiver` checks two\nheaders:\n\n- `X-OpenProse-Delivery`: a new id for each event, 1 to 200 letters, digits,\n `.`, `_`, `:` or `-`, starting with a letter or digit. A retry sends the\n same id and the same body.\n- `X-OpenProse-Signature`: `sha256=` and the lowercase hex HMAC-SHA256, keyed\n with the signing secret, of the delivery id, one newline and the exact body\n bytes. Sign the bytes you send; do not reserialize the body after signing.\n\n```sh\nsig=$(printf '%s\\n%s' \"$DELIVERY_ID\" \"$BODY\" | openssl dgst -sha256 -hmac \"$SIGNING_SECRET\" | sed 's/^.* //')\ncurl -X POST \"$ENDPOINT_URL\" -H 'Content-Type: application/json' \\\n -H \"X-OpenProse-Delivery: $DELIVERY_ID\" -H \"X-OpenProse-Signature: sha256=$sig\" \\\n --data-binary \"$BODY\"\n```\n\n`202` with `\"accepted\":true` means the event was accepted, not that a run\nfinished; `\"test_only\":true` means no run was started. A missing or malformed\ndelivery id, or a body that is not JSON, is `400`; a body over 256 KiB is\n`413`; a wrong signature is `401`. `prose cli job deliveries JOB_ID --json`\nlists the events received, including those rejected for a wrong signature.\n" }, "stderr": { "text": "" diff --git a/cli/conformance/cases/service/framework/service-guide-json.json b/cli/conformance/cases/service/framework/service-guide-json.json index 9b9e15a1..b3c848a1 100644 --- a/cli/conformance/cases/service/framework/service-guide-json.json +++ b/cli/conformance/cases/service/framework/service-guide-json.json @@ -88,7 +88,7 @@ { "id": "recipes", "title": "Recipes", - "body": "### Submit and wait\n\n```sh\nprose cli run quote --json\nprose cli run submit hello.prose.md --input topic=otters --preview\nprose cli run submit hello.prose.md --input topic=otters --yes --json > run.json\njq -r '.result.run.response' run.json\n```\n\n`cli run quote` is the wallet hold a run reserves, not its price; the settled\nprice is `result.run.price_cents`. A saved program runs with\n`--from SLUG` (your own) or `--from OWNER/SLUG@REV`; every program record\n(`program list|show|save|revisions --json`) carries that pinned reference as\n`ref`, and `prose cli program show SLUG@1 --json` resolves a revision number\nof your own program to it. On exit 21, rerun\n`problem.details.resumeArgv`; for a long run submit with `--detach` and follow\nwith `prose cli run watch RUN_ID --wait 10m --json`.\n\n### Publish a result\n\nA published result is a completed run of a saved revision of a public\nprogram that wrote `outputs/result.json`.\n\n```sh\nprose cli program save haiku haiku.prose.md --json\nprose cli run submit --from haiku --yes --json\nprose cli program visibility haiku public --preview\nprose cli result publish haiku --run RUN_ID --yes --json\nprose cli result show haiku --json\n```\n\nMaking a program public exposes its source; the CLI never adds `--yes` to that\nstep for you. Reading published results needs no key; a bare `SLUG` (your\nown program) is resolved with your key first, and without a publication id\n`result show` reads the newest.\n\n### Create a webhook job\n\nA job starts runs on its own. The spec is snake_case JSON; a test-mode webhook\nreceives events and never starts runs.\n\n```sh\nprintf '%s' '{\"type\":\"webhook\",\"name\":\"my-hook\",\"delivery_mode\":\"test\"}' > hook.json\nprose cli job create --spec-file hook.json --preview\nprose cli job create --spec-file hook.json --yes --json\nprose cli job deliveries JOB_ID --json\n```\n\n`result.endpoint` and `result.signing_secret` appear only in the create result\n(and in `cli job rotate-secret`); store them then. `result.endpointUrl` is the\nabsolute URL to configure in the sender; `cli job show` repeats it. `prose cli job create --help`\nlists the schedule and webhook spec keys; `prose cli job list --json` lists the\njob types. The keys each type accepts are the `spec` of `job.create` in\n`prose cli service operations --json`; a spec is checked against it before any\nrequest and every problem is reported at once in `details.violations`. A\nwebhook with no `program_ref` starts no runs, so its plan has effect `write`\nand no hold." + "body": "### Submit and wait\n\n```sh\nprose cli run quote --json\nprose cli run submit hello.prose.md --input topic=otters --preview\nprose cli run submit hello.prose.md --input topic=otters --yes --json > run.json\njq -r '.result.run.response' run.json\n```\n\n`cli run quote` is the wallet hold a run reserves, not its price; the settled\nprice is `result.run.price_cents`. A saved program runs with\n`--from SLUG` (your own) or `--from OWNER/SLUG@REV`; every program record\n(`program list|show|save|revisions --json`) carries that pinned reference as\n`ref`, and `prose cli program show SLUG@1 --json` resolves a revision number\nof your own program to it. On exit 21, rerun\n`problem.details.resumeArgv`; for a long run submit with `--detach` and follow\nwith `prose cli run watch RUN_ID --wait 10m --json`.\n\n### Publish a result\n\nA published result is a completed run of a saved revision of a public\nprogram that wrote `outputs/result.json`.\n\n```sh\nprose cli program save haiku haiku.prose.md --json\nprose cli run submit --from haiku --yes --json\nprose cli program visibility haiku public --preview\nprose cli result publish haiku --run RUN_ID --yes --json\nprose cli result show haiku --json\n```\n\nMaking a program public exposes its source; the CLI never adds `--yes` to that\nstep for you. Reading published results needs no key; a bare `SLUG` (your\nown program) is resolved with your key first, and without a publication id\n`result show` reads the newest.\n\n### Create a webhook job\n\nA job starts runs on its own. The spec is snake_case JSON; a test-mode webhook\nreceives events and never starts runs.\n\n```sh\nprintf '%s' '{\"type\":\"webhook\",\"name\":\"my-hook\",\"delivery_mode\":\"test\"}' > hook.json\nprose cli job create --spec-file hook.json --preview\nprose cli job create --spec-file hook.json --yes --json\nprose cli job deliveries JOB_ID --json\n```\n\n`result.endpoint` and `result.signing_secret` appear only in the create result\n(and in `cli job rotate-secret`); store them then. `result.endpoint_url` is the\nabsolute URL to configure in the sender; `cli job show` repeats it. `prose cli job create --help`\nlists the schedule and webhook spec keys; `prose cli job list --json` lists the\njob types. The keys each type accepts are the `spec` of `job.create` in\n`prose cli service operations --json`; a spec is checked against it before any\nrequest and every problem is reported at once in `details.violations`. A\nwebhook with no `program_ref` starts no runs, so its plan has effect `write`\nand no hold.\n\nA sender POSTs the event as JSON (`Content-Type: application/json`, at most\n256 KiB) to the endpoint URL. A webhook created without `receiver` checks two\nheaders:\n\n- `X-OpenProse-Delivery`: a new id for each event, 1 to 200 letters, digits,\n `.`, `_`, `:` or `-`, starting with a letter or digit. A retry sends the\n same id and the same body.\n- `X-OpenProse-Signature`: `sha256=` and the lowercase hex HMAC-SHA256, keyed\n with the signing secret, of the delivery id, one newline and the exact body\n bytes. Sign the bytes you send; do not reserialize the body after signing.\n\n```sh\nsig=$(printf '%s\\n%s' \"$DELIVERY_ID\" \"$BODY\" | openssl dgst -sha256 -hmac \"$SIGNING_SECRET\" | sed 's/^.* //')\ncurl -X POST \"$ENDPOINT_URL\" -H 'Content-Type: application/json' \\\n -H \"X-OpenProse-Delivery: $DELIVERY_ID\" -H \"X-OpenProse-Signature: sha256=$sig\" \\\n --data-binary \"$BODY\"\n```\n\n`202` with `\"accepted\":true` means the event was accepted, not that a run\nfinished; `\"test_only\":true` means no run was started. A missing or malformed\ndelivery id, or a body that is not JSON, is `400`; a body over 256 KiB is\n`413`; a wrong signature is `401`. `prose cli job deliveries JOB_ID --json`\nlists the events received, including those rejected for a wrong signature." } ] }, diff --git a/cli/conformance/cases/service/framework/service-guide-jsonl.json b/cli/conformance/cases/service/framework/service-guide-jsonl.json index 1d2a9515..6fc8706a 100644 --- a/cli/conformance/cases/service/framework/service-guide-jsonl.json +++ b/cli/conformance/cases/service/framework/service-guide-jsonl.json @@ -94,7 +94,7 @@ { "id": "recipes", "title": "Recipes", - "body": "### Submit and wait\n\n```sh\nprose cli run quote --json\nprose cli run submit hello.prose.md --input topic=otters --preview\nprose cli run submit hello.prose.md --input topic=otters --yes --json > run.json\njq -r '.result.run.response' run.json\n```\n\n`cli run quote` is the wallet hold a run reserves, not its price; the settled\nprice is `result.run.price_cents`. A saved program runs with\n`--from SLUG` (your own) or `--from OWNER/SLUG@REV`; every program record\n(`program list|show|save|revisions --json`) carries that pinned reference as\n`ref`, and `prose cli program show SLUG@1 --json` resolves a revision number\nof your own program to it. On exit 21, rerun\n`problem.details.resumeArgv`; for a long run submit with `--detach` and follow\nwith `prose cli run watch RUN_ID --wait 10m --json`.\n\n### Publish a result\n\nA published result is a completed run of a saved revision of a public\nprogram that wrote `outputs/result.json`.\n\n```sh\nprose cli program save haiku haiku.prose.md --json\nprose cli run submit --from haiku --yes --json\nprose cli program visibility haiku public --preview\nprose cli result publish haiku --run RUN_ID --yes --json\nprose cli result show haiku --json\n```\n\nMaking a program public exposes its source; the CLI never adds `--yes` to that\nstep for you. Reading published results needs no key; a bare `SLUG` (your\nown program) is resolved with your key first, and without a publication id\n`result show` reads the newest.\n\n### Create a webhook job\n\nA job starts runs on its own. The spec is snake_case JSON; a test-mode webhook\nreceives events and never starts runs.\n\n```sh\nprintf '%s' '{\"type\":\"webhook\",\"name\":\"my-hook\",\"delivery_mode\":\"test\"}' > hook.json\nprose cli job create --spec-file hook.json --preview\nprose cli job create --spec-file hook.json --yes --json\nprose cli job deliveries JOB_ID --json\n```\n\n`result.endpoint` and `result.signing_secret` appear only in the create result\n(and in `cli job rotate-secret`); store them then. `result.endpointUrl` is the\nabsolute URL to configure in the sender; `cli job show` repeats it. `prose cli job create --help`\nlists the schedule and webhook spec keys; `prose cli job list --json` lists the\njob types. The keys each type accepts are the `spec` of `job.create` in\n`prose cli service operations --json`; a spec is checked against it before any\nrequest and every problem is reported at once in `details.violations`. A\nwebhook with no `program_ref` starts no runs, so its plan has effect `write`\nand no hold." + "body": "### Submit and wait\n\n```sh\nprose cli run quote --json\nprose cli run submit hello.prose.md --input topic=otters --preview\nprose cli run submit hello.prose.md --input topic=otters --yes --json > run.json\njq -r '.result.run.response' run.json\n```\n\n`cli run quote` is the wallet hold a run reserves, not its price; the settled\nprice is `result.run.price_cents`. A saved program runs with\n`--from SLUG` (your own) or `--from OWNER/SLUG@REV`; every program record\n(`program list|show|save|revisions --json`) carries that pinned reference as\n`ref`, and `prose cli program show SLUG@1 --json` resolves a revision number\nof your own program to it. On exit 21, rerun\n`problem.details.resumeArgv`; for a long run submit with `--detach` and follow\nwith `prose cli run watch RUN_ID --wait 10m --json`.\n\n### Publish a result\n\nA published result is a completed run of a saved revision of a public\nprogram that wrote `outputs/result.json`.\n\n```sh\nprose cli program save haiku haiku.prose.md --json\nprose cli run submit --from haiku --yes --json\nprose cli program visibility haiku public --preview\nprose cli result publish haiku --run RUN_ID --yes --json\nprose cli result show haiku --json\n```\n\nMaking a program public exposes its source; the CLI never adds `--yes` to that\nstep for you. Reading published results needs no key; a bare `SLUG` (your\nown program) is resolved with your key first, and without a publication id\n`result show` reads the newest.\n\n### Create a webhook job\n\nA job starts runs on its own. The spec is snake_case JSON; a test-mode webhook\nreceives events and never starts runs.\n\n```sh\nprintf '%s' '{\"type\":\"webhook\",\"name\":\"my-hook\",\"delivery_mode\":\"test\"}' > hook.json\nprose cli job create --spec-file hook.json --preview\nprose cli job create --spec-file hook.json --yes --json\nprose cli job deliveries JOB_ID --json\n```\n\n`result.endpoint` and `result.signing_secret` appear only in the create result\n(and in `cli job rotate-secret`); store them then. `result.endpoint_url` is the\nabsolute URL to configure in the sender; `cli job show` repeats it. `prose cli job create --help`\nlists the schedule and webhook spec keys; `prose cli job list --json` lists the\njob types. The keys each type accepts are the `spec` of `job.create` in\n`prose cli service operations --json`; a spec is checked against it before any\nrequest and every problem is reported at once in `details.violations`. A\nwebhook with no `program_ref` starts no runs, so its plan has effect `write`\nand no hold.\n\nA sender POSTs the event as JSON (`Content-Type: application/json`, at most\n256 KiB) to the endpoint URL. A webhook created without `receiver` checks two\nheaders:\n\n- `X-OpenProse-Delivery`: a new id for each event, 1 to 200 letters, digits,\n `.`, `_`, `:` or `-`, starting with a letter or digit. A retry sends the\n same id and the same body.\n- `X-OpenProse-Signature`: `sha256=` and the lowercase hex HMAC-SHA256, keyed\n with the signing secret, of the delivery id, one newline and the exact body\n bytes. Sign the bytes you send; do not reserialize the body after signing.\n\n```sh\nsig=$(printf '%s\\n%s' \"$DELIVERY_ID\" \"$BODY\" | openssl dgst -sha256 -hmac \"$SIGNING_SECRET\" | sed 's/^.* //')\ncurl -X POST \"$ENDPOINT_URL\" -H 'Content-Type: application/json' \\\n -H \"X-OpenProse-Delivery: $DELIVERY_ID\" -H \"X-OpenProse-Signature: sha256=$sig\" \\\n --data-binary \"$BODY\"\n```\n\n`202` with `\"accepted\":true` means the event was accepted, not that a run\nfinished; `\"test_only\":true` means no run was started. A missing or malformed\ndelivery id, or a body that is not JSON, is `400`; a body over 256 KiB is\n`413`; a wrong signature is `401`. `prose cli job deliveries JOB_ID --json`\nlists the events received, including those rejected for a wrong signature." } ] }, diff --git a/cli/shared/schemas/README.md b/cli/shared/schemas/README.md index d36b7fab..040f6c63 100644 --- a/cli/shared/schemas/README.md +++ b/cli/shared/schemas/README.md @@ -68,7 +68,7 @@ named by each operation's `output.schema` in service field names verbatim (snake_case; camelCase for jobs) and drop every field the projection does not name. Signed `file_urls`, `customer_id`, webhook endpoints and secrets are never projected, except the secret-bearing field of -the one operation that creates it (and a webhook's `endpointUrl` in `job show` +the one operation that creates it (and a webhook's `endpoint_url` in `job show` when its endpoint is the secret-free job-id path). No property name may match `/cost/i`; open maps exclude such names with `propertyNames`. Run the checks with `python3 -m unittest cli/shared/tests/test_service_contract.py`. diff --git a/cli/shared/service/guide.v1.md b/cli/shared/service/guide.v1.md index 59c97fb4..0a27f7be 100644 --- a/cli/shared/service/guide.v1.md +++ b/cli/shared/service/guide.v1.md @@ -351,7 +351,7 @@ prose cli job deliveries JOB_ID --json ``` `result.endpoint` and `result.signing_secret` appear only in the create result -(and in `cli job rotate-secret`); store them then. `result.endpointUrl` is the +(and in `cli job rotate-secret`); store them then. `result.endpoint_url` is the absolute URL to configure in the sender; `cli job show` repeats it. `prose cli job create --help` lists the schedule and webhook spec keys; `prose cli job list --json` lists the job types. The keys each type accepts are the `spec` of `job.create` in @@ -359,3 +359,27 @@ job types. The keys each type accepts are the `spec` of `job.create` in request and every problem is reported at once in `details.violations`. A webhook with no `program_ref` starts no runs, so its plan has effect `write` and no hold. + +A sender POSTs the event as JSON (`Content-Type: application/json`, at most +256 KiB) to the endpoint URL. A webhook created without `receiver` checks two +headers: + +- `X-OpenProse-Delivery`: a new id for each event, 1 to 200 letters, digits, + `.`, `_`, `:` or `-`, starting with a letter or digit. A retry sends the + same id and the same body. +- `X-OpenProse-Signature`: `sha256=` and the lowercase hex HMAC-SHA256, keyed + with the signing secret, of the delivery id, one newline and the exact body + bytes. Sign the bytes you send; do not reserialize the body after signing. + +```sh +sig=$(printf '%s\n%s' "$DELIVERY_ID" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | sed 's/^.* //') +curl -X POST "$ENDPOINT_URL" -H 'Content-Type: application/json' \ + -H "X-OpenProse-Delivery: $DELIVERY_ID" -H "X-OpenProse-Signature: sha256=$sig" \ + --data-binary "$BODY" +``` + +`202` with `"accepted":true` means the event was accepted, not that a run +finished; `"test_only":true` means no run was started. A missing or malformed +delivery id, or a body that is not JSON, is `400`; a body over 256 KiB is +`413`; a wrong signature is `401`. `prose cli job deliveries JOB_ID --json` +lists the events received, including those rejected for a wrong signature. diff --git a/docs/service/jobs.md b/docs/service/jobs.md index 5d82db50..aa39fb2b 100644 --- a/docs/service/jobs.md +++ b/docs/service/jobs.md @@ -93,6 +93,44 @@ printf '%s' '{"type":"webhook","delivery_mode":"test","name":"my-hook"}' \ results also carry `endpoint_url`, the absolute URL to configure in the sender (the service origin + `endpoint`). +## Sending events to a webhook + +The sender POSTs JSON (`Content-Type: application/json`, at most 256 KiB) to +`endpoint_url`. A webhook created without `receiver` checks two headers: + +- `X-OpenProse-Delivery`: a new id for each event matching + `[A-Za-z0-9][A-Za-z0-9._:-]{0,199}`. A retry reuses the id and the exact body. +- `X-OpenProse-Signature`: `sha256=` followed by the lowercase hex + HMAC-SHA256 of `deliveryId + "\n" + rawBody`, keyed with the UTF-8 + `signing_secret`. Sign the bytes you send; do not reserialize the body. + +```sh +sig=$(printf '%s\n%s' "$DELIVERY_ID" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | sed 's/^.* //') +curl -X POST "$ENDPOINT_URL" -H 'Content-Type: application/json' \ + -H "X-OpenProse-Delivery: $DELIVERY_ID" -H "X-OpenProse-Signature: sha256=$sig" \ + --data-binary "$BODY" +``` + +| Response | Meaning | +| --- | --- | +| `202 {"accepted":true,"duplicate":false}` | Accepted. This acknowledges receipt, not run completion. | +| `202` with `"test_only":true` | Test mode: recorded, no run started. Test mode does not deduplicate: a repeated id is recorded again. | +| `202` with `"duplicate":true` | Live mode: a retry of a delivery already accepted. | +| `400` | Missing or malformed `X-OpenProse-Delivery`, or a body that is not valid UTF-8 JSON. | +| `401` | Wrong signature. `job deliveries` records it as `rejected` with reason `invalid_signature`. | +| `409` | Live mode: a reused delivery id with a different body, or no contract attached. Don't retry it unchanged. `"reason":"configuration_changed"` means the webhook changed during the request; retry it. | +| `413` | Body over 256 KiB. | + +Retry only network failures, `429` and `5xx`, with backoff, keeping the id +and body. `job deliveries JOB_ID` shows what arrived. The job's +`last_event_at` records live deliveries only, so it stays empty in test mode. +A webhook can be live only with a contract: create it with `program_ref`, or +run `job contract attach` before `job update` sets `"delivery_mode":"live"`. +Otherwise the service answers `SERVICE_REQUEST_REJECTED` ("Runs stay +disabled. Connect a contract to this webhook first."). +`openssl -hmac` puts the secret in the process arguments; on a shared host, +sign with a library instead. + ## Reading jobs `job list` returns `{jobs, max_jobs, job_limit, types}` (each type