hikyo
Documentation

Deployment adapters

Push selected published values to Forgejo, GitHub Actions, GitLab, Cloudflare, AWS Secrets Manager, Vault/OpenBao KV v2, or a sealed webhook receiver without reading provider values back.

If a job can fetch values at runtime, use workload federation. Use a deployment adapter only for static values that must exist before the first fetch.

For a CI job that creates and tears down its own environments, use the pull-request preview guide.

The deployment adapters are one-way and value-blind. Forgejo and GitHub Actions write to repository, organization, or GitHub environment namespaces; GitLab writes project or group CI/CD variables. Cloudflare writes encrypted Workers secrets or Pages variables. AWS Secrets Manager writes into one account and region, and Vault/OpenBao KV v2 writes one secret per key. CI variable planning uses Hikyo’s ownership ledger, without variable GET or list endpoints. Cloudflare reads settings but decodes only names and types, discarding plaintext fields. AWS and Vault read metadata only; their provider sections explain how metadata detects external changes. CI adapters cannot detect provider-side value drift; run a converge to restore published Hikyo values. The sealed webhook adapter pushes to a generic HTTPS receiver an instance administrator has pinned; it has no read path at all.

Requirements

  • Forgejo 1.21 or newer.
  • A repository target PAT with write:repository, or an organization target PAT with write:organization. Forgejo write scopes also permit GET requests, but Hikyo’s variable-read operations do not exist in the client.
  • GitHub requires a fine-grained PAT (github_pat_); classic ghp_ PATs are refused before provider contact. Grant the fine-grained Secrets and Variables write permissions for the chosen repository or organization. Creating a missing environment additionally requires Administration (write): either pre-create it in GitHub and keep the delivery token narrow, or grant that permission and explicitly select environment creation in the web form, or pass --create-environment to hikyo adapter create / hikyo adapter target add. Without consent, Hikyo never creates a missing environment.
  • manage-adapters for inspection and lifecycle operations. Creating, widening, adopting, changing a prefix, replacing a credential, and manually syncing also require reveal authority and adapter reauthentication for every environment synchronized by that adapter.

The credential is write-only after entry. hikyo adapter create and hikyo adapter credential set read it from a no-echo terminal by default, or from --stdin or --value-file. There is no credential-value argument.

Targets and names

An adapter owns one provider origin and credential. Each target binds one Hikyo environment to one provider destination and an explicit, non-empty key subset. Organization targets are wide: every repository in that organization allowed by its all, private, or selected visibility can consume the organization-level values. Selected repositories are stored and verified by immutable numeric id. Moving to all, changing between private and selected, or adding selected ids is a widening ceremony; all to private/selected and selected-id removal are plain narrowing. That breadth is a storage-level property, not an override guarantee: a repository- level secret or variable with the same name shadows the organization-level value for workflows in that repository.

For GitHub, a repository environment is the recommended multi-environment shape: map dev, stage, and production to separate GitHub environments with no prefix. A missing environment is created during the reauthenticated target-add flow with an empty settings body, then its repository id and environment id are pinned. Later absence or either id changing is refused; Hikyo never redirects the write to repository scope. Free-plan private repositories without environments must be reconfigured explicitly to repository scope with prefixes. Workflow output for an environment target includes the exact environment: selection before the canonical-name mapping.

Effective provider names are prefix + canonical key name. The optional prefix applies to every key and to the MANAGED_BY_HIKYO sentinels. Effective name sets for targets sharing a destination must be disjoint. Use distinct prefixes when multiple environments feed one repository.

Applications still consume canonical names. Generate the required workflow mapping without exposing values:

hikyo adapter target show tgt_123 --format workflow

For example, a PROD_ target produces mappings from DATABASE_URL to ${{ secrets.PROD_DATABASE_URL }}. Local execution, rendered env files, and the Kubernetes operator continue to use DATABASE_URL.

Lifecycle

Create validates the provider connection and destination identity before storing the adapter credential and first target. A missing GitHub environment is the configure flow’s first provider write: a durable generation-1 effect fence and correlated adapter.push_intent exist before its settings-free PUT, and adapter.push_outcome closes that fence.

GitHub storage caps are 100 secrets/500 variables per repository, 1,000/1,000 per organization, and 100/100 per environment. Plans warn exactly from the complete secret-name list and report variables as a lower bound because Hikyo cannot safely read provider variables. Organization workflows expose only the first 100 secrets alphabetically; combined organization and repository variables over 256 KB can also truncate at consumption time. Plan/sync warnings name both limits.

GitHub rejects empty variable values. Sync refuses an empty configuration value by key name before any provider request; it does not substitute whitespace or silently omit the variable. Plans remain value-blind. Variables write permission is first proven by the sync sentinel, since connection checks cannot safely read provider variables or mutate them.

The GitHub delivery limits are 48,000 UTF-8 bytes per variable and 47,952 plaintext bytes per secret. These effective limits are pinned by real API boundary probes; they are not 48 KiB. Sync rejects larger values by key name before contacting the provider. It never chunks or re-encodes a value to fit.

Published environment changes automatically queue a newest-wins converge for intersecting targets under their recorded authority. adapter plan reports names and dispositions only. Existing provider names are never captured silently: adoption requires an exact conflict artifact, generation, destination, and (surface, name) pairs. If multiple exact-match artifacts are eligible, pass --artifact explicitly.

hikyo adapter show includes every target’s sync status, converged revision, failed names, and pending conflict artifacts. Its table view prints the pending surface:name pairs; JSON retains the full artifact id, generation, destination, entries, and creation time required for an exact adoption.

Origin and destination changes are asynchronous scrub-before-switch moves. They block new pushes, scrub the old route, activate and test the pending route, then converge it. The update response contains a move id; API clients poll GET /api/v1/orgs/{org}/projects/{project}/adapter-moves/{move} for the state, per-target scrub/activation/converge jobs, pending route, and orphaned names. Credentials never appear in a move response. --keep-remote skips the scrub, releases custody immediately, and enumerates the resulting orphaned names. When an old credential is dead, the move uses that same orphan-and-release rule before proceeding.

Transient activation failures retry. An invalid pending credential or a collision on the new route leaves the move in attention_required; Hikyo does not silently roll back. PATCHing the concrete move replaces either its pending origin plus write-only credential or its complete pending target route, then resumes activation. DELETEing that move atomically cancels it, restores the old route, and queues old-route reconvergence. The CLI requires the move id through --move for either recovery path, so pending and live credential replacement cannot be confused. The browser starts an origin move from the adapter panel (Change origin), carries the move id in the page URL, polls the move in a Route move pane until the server settles it, and offers exactly the two exits of an attention-required move: Resume with a new credential and Cancel move. Target environments are immutable: cross-environment movement is remove plus add, so authority and custody are never silently rebound.

Adapter reauthentication is distinct from value reveal. Its signed windows bind purpose adapter, the exact operation, and the complete environment set. One TOTP proof covers environments whose effective window is nonzero; each effective-zero environment requires its own WebAuthn proof. The CLI handoff is state- and PKCE-bound, has a single-use authorization code, and rotates the CLI bearer. The CLI binds an ephemeral 127.0.0.1 callback before opening /reauth/cli?transaction=<opaque-state>. The browser reads only display policy, performs the required ceremonies, and returns only code plus the same state to that exact callback. Only redemption returns the bearer; the CLI replaces its mode-0600 local artifact without printing or logging it.

Removing a target or adapter scrubs ledger-owned names by default, with sentinels last. --keep-remote releases custody instead and loudly lists the orphaned names. Remove targets while the PAT still authenticates. If a dead or revoked credential prevents scrub, Hikyo completes local teardown but reports the remote names as orphaned.

GitLab CI/CD variables

The GitLab adapter writes project or group CI/CD variables on gitlab.com or a self-managed GitLab 16.0 or newer. GitLab’s variable reads return stored values, so its client has no read or list operation of any kind: every existence and ownership decision comes from Hikyo’s ledger, and adapter plan reports keys Hikyo does not yet own as unknown-until-sync. A key already held by someone else is discovered only when GitLab refuses the create as already taken. That is recorded as a conflict for explicit adoption, and the existing variable is never overwritten.

Token. Use a project or group access token with the api scope and the Maintainer role (Owner for group destinations, which GitLab requires to manage group variables). A personal access token is refused by default because it can act as its owner everywhere they have access; creating the adapter with --allow-personal-token (or the browser checkbox) accepts one and records that choice in the audit trail. Tokens with sudo or admin_mode are always refused. An expiry within 14 days shows as a target warning; an expired token fails the converge as an authentication error.

Destinations. --kind project takes --owner NAMESPACE --repo PROJECT; --kind group takes --owner GROUP/PATH. Non-default group environment scopes require GitLab Premium or Ultimate; use * on GitLab Free (GitLab API). Both are pinned by numeric id, and a project or group that moves or is renamed is refused until an operator reconfigures the target. Each target has one GitLab environment scope, --scope (default *). The scope is fixed once the target exists; to change it, remove the target (retain or prune) and add a new one. One project can receive the same key under different scopes from different Hikyo environments, which is the usual shape:

hikyo adapter create --provider gitlab --origin https://gitlab.example.com \
  --env prod --kind project --owner platform --repo api --scope production \
  --protected --include '*'
hikyo adapter target add --adapter adp_123 --env staging --kind project \
  --owner platform --repo api --scope staging --include '*'

Flags. Secret-classified keys are always written masked. --hidden creates them masked and hidden (GitLab 17.4 or newer; an older GitLab is refused by name rather than downgraded). --protected limits every managed variable to protected branches and tags; removing it later requires the full reauthentication ceremony because it widens who can read the values. Values are written raw by default, so a $ is delivered byte-exactly; --expand-variables opts into GitLab’s variable expansion.

Masking constraints. Hikyo applies a conservative masking policy: a single-line value of at least 8 characters drawn from A-Z a-z 0-9 _ @ : . ~ + / = -. Hikyo checks every selected secret against that rule before a job is queued and fails by key name without printing the value. Exclude nonconforming secrets from the target; do not reclassify sensitive keys as configuration, which is unmasked. GitLab allows a broader alphabet with expansion disabled, but Hikyo retains this stricter rule. Hikyo never ships a secret unmasked. Keys must match [A-Za-z_][A-Za-z0-9_]*, at most 255 characters, and may not start with CI_ or GITLAB_.

Self-managed TLS. --ca-bundle-file adds PEM trust anchors for an internal CA. --spki-pin pins the server’s base64(sha256(SubjectPublicKeyInfo)), the same value openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64 prints. The pin is checked after normal certificate verification, never instead of it. Both are fixed for the adapter’s lifetime and move with it through an origin change; to change them, create a new adapter. A self-managed instance on a private address also needs its CIDR in the adapter egress policy.

The MANAGED_BY_HIKYO sentinel is a single unprotected variable per destination and scope, written first and removed last. Rate limits honour Retry-After and RateLimit-Reset.

Multiple targets: selection, health, and control

One published revision fans out to every target configured for its environment, across every adapter kind, through one immutable job per target pinned to that revision, the target’s configuration generation, and the adapter’s recorded authority. Targets never share a job: a failure, a pause, or a conflict at one destination cannot block, roll back, or falsely mark another target successful.

Selecting keys. Membership is always an explicit set of immutable key ids. --keys names ids; --names names keys exactly; --include and --exclude are bounded glob patterns over key names (DB_*, *_TEST); --classification keeps only secret or config keys. Patterns and classification are resolved to ids when the target is saved and are not stored: a key created later that would have matched is never added on its own. --exclude narrows only what the patterns selected, never an explicit id or name. Widening the resolved set is a routing mutation and carries the full reauthentication ceremony.

hikyo adapter target add --adapter adp_123 --env prod --kind repository \
  --owner acme --repo app --prefix PROD_ --include 'DB_*' --exclude '*_TEST' \
  --classification secret

Naming. The per-target prefix is the one structural name mapping: every name the target writes is prefix + canonical name, validated whole against the provider grammar and collision-checked case-insensitively across the target and against every other target on the same destination. Values are never transformed. There is no per-key rename table by design; see the deployment-adapter ADR.

Health. adapter target list and the target detail report one of:

StatusMeaning
neverNo converge has run.
pendingA converge is queued and not yet claimed.
convergingA worker holds the job.
convergedThe destination holds converged_revision.
degradedThe last attempt failed but an earlier revision converged; the destination still holds it.
failedThe last attempt failed and nothing has converged yet.
pausedAn operator paused the target.

Beside the status: the last successful and last attempted revisions and time, a bounded error class (auth, network, conflict, provider_limit, provider_ambiguous, refused; never a provider response body), the retry time when a retry is queued, and attention when the destination disagrees with the ownership ledger in a way only an operator can settle (an unowned name in the way, a destination whose identity moved, orphaned names). Attention clears on the next successful converge.

Pause and resume. hikyo adapter target pause <target> stops every push without deleting owned destination state: the active job is superseded, a worker still inside it is fenced, publishes skip the target, and a manual sync is refused. Pause narrows what Hikyo does and carries manage-adapters alone. hikyo adapter target resume <target> runs the sync ceremony, queues one catch-up converge, and prints the published revision it reaches.

Resync. hikyo adapter sync --target <target> is the idempotent resync: newest wins, a second request supersedes the first, and neither bypasses adoption nor prunes beyond the ledger.

Removal keeps the explicit retain-or-prune decision described above. The browser offers the same choice in a dialog with neither option preselected, for one target (Remove) and for the whole adapter (Delete adapter).

Credential custody and probes. The adapter panel replaces the write-only credential (Replace credential, behind the credential-set ceremony) and destroys custody (Revoke credential, stating that a later remote scrub may then be impossible). The target detail runs Plan (names and dispositions only) and Test connection (provider version, destination identity and credential expiry; no value, no write), the browser forms of adapter plan and adapter test.

Operators. hikyo doctor reports the instance-wide counts of targets that need attention, failed targets, paused targets, and queued jobs, and the same four label-free gauges appear on /metrics as hikyo_adapter_targets_failed, hikyo_adapter_targets_paused, hikyo_adapter_targets_attention, and hikyo_adapter_jobs_queued.

Crash windows. If a worker dies between a push’s INTENT and its OUTCOME, the next claim of that target settles the effect as OUTCOME unknown, correlated to the job that opened it; the ownership row stays presumed written and the retry converges it. On a multi-node installation every worker competes for jobs under the outbox lease; a worker that loses its lease can no longer push, mark success, retry, or scrub.

Cloudflare Workers and Pages

The cloudflare provider synchronizes into two explicit destination kinds that share one credential and one ownership model:

Kind--owner / --account--repo / --script / --pages-project--destination-environment
workers-script32-character account idWorkers script nameempty
pages-project32-character account idPages project namepreview or production
hikyo adapter create --provider cloudflare --env prod --kind workers-script \
  --account 0123456789abcdef0123456789abcdef --script api --keys key_1,key_2 --stdin
hikyo adapter target add --adapter <adapter> --env preview --kind pages-project \
  --account 0123456789abcdef0123456789abcdef --pages-project site \
  --destination-environment preview --keys key_1

The origin is always https://api.cloudflare.com; the CLI fills it in. Preview and production of one Pages project are separate targets with separate sentinels and ownership ledgers, so a preview sync can never touch production.

Every write is secret_text. Workers secrets are written with type: secret_text; Pages variables are written into deployment_configs.<environment>.env_vars with type: secret_text. Keys classified config are delivered as secrets too. The client has no operation that writes plain_text, so a destination that cannot keep a value secret is refused rather than downgraded. The management sentinel MANAGED_BY_HIKYO is a secret written first and removed last.

Credential. Use a scoped API token sent as Authorization: Bearer. Grant Workers Scripts: Edit for Workers targets and Cloudflare Pages: Edit for Pages targets, on exactly one account. Before activation Hikyo verifies the token is active and unexpired and that it reaches exactly the configured account. Global API Keys, email/key pairs, and tokens that reach more than one account are refused. The verified expiry is recorded as the credential expiry, and every sync re-verifies the token before its first write.

What Hikyo reads. Workers: the script listing (for the script’s immutable tag) and the script settings, decoded into binding names only. Settings return plain_text binding values; they are never decoded, logged, or compared, and the response buffer is cleared. Pages: the project object, decoded into the project id and the names and types of the selected environment’s variables only. Pages returns plain_text values in that response; they are never decoded, logged, or compared, and the response buffer is cleared. Secret values are never returned by Cloudflare and never requested.

Ownership and conflicts. Both Cloudflare writes are upserts, so Hikyo re-reads the destination names immediately before each first write of a name and refuses any name that already exists and is not in the ownership ledger, including Workers bindings of any type and Pages plain_text variables. Adopt an existing name through the usual plan-and-adopt flow. A script or project that is renamed, deleted, or recreated under the same name changes its immutable id; the target then fails with a destination-identity refusal and needs reconfiguration.

Side effects. Every Workers secret write or delete deploys a new version of the script. Pages variable changes apply from the next deployment of that environment; existing deployments keep their values. Target status carries both as warnings. Writes are one name per request so each effect has its own INTENT and OUTCOME; Pages PATCH merges per key and a delete sends the name as null.

Failures. HTTP 429 honours Retry-After (five minutes without one, the length of Cloudflare’s request window). A 2xx response whose envelope does not report success: true is recorded as an unknown outcome and replayed as an update. Hikyo cannot detect a value edited in the Cloudflare dashboard; the next converge overwrites owned names with the published Hikyo value.

Private origins and GHES

Adapters deny private, loopback, link-local, documentation, and reserved addresses by default. Operators can add origin-scoped exceptions with HIKYO_ADAPTER_EGRESS_POLICY_FILE; see server configuration. HTTPS, certificate validation, DNS rebinding protection, and the no-redirect rule remain mandatory. GitHub Enterprise Server uses its configured HTTPS API base and inherits the GitHub contract best-effort; github.com contract pins do not claim every GHES version behaves identically.

AWS Secrets Manager

The aws-secrets-manager adapter writes selected published values into AWS Secrets Manager in one account and region. Hikyo stays the source of truth: the adapter cannot read a value back. Its provider interface has no GetSecretValue, BatchGetSecretValue, or GetRandomPassword, and a test fails the build if production code ever names one.

Destinations

Each target picks one destination kind:

KindWhat lands in AWS
json-objectOne secret named --secret whose value is a JSON object: one member per selected key, named name prefix + key, members sorted, no whitespace. Values are JSON strings, so parsing the document returns the exact published bytes.
per-keyOne secret per selected key, named path prefix + name prefix + key, for example prod/app/DATABASE_URL. The optional path prefix must end in /.

The destination owner is the 12-digit AWS account id. Hikyo checks it against STS GetCallerIdentity and against every secret ARN, so a credential or endpoint that lands in another account or region is refused as a destination move rather than followed. An optional customer KMS key (--kms-key) is applied when Hikyo creates a secret; changing it, the mode, the secret name, or the path is a destination move with the usual scrub-before-switch.

Names are refused, never rewritten: AWS allows A-Z a-z 0-9 / _ + = . @ -, up to 512 bytes, and Hikyo also refuses a name ending in - plus six characters, which AWS ARN lookups confuse with the random ARN suffix. A value must be valid UTF-8 without NUL; per-key values must be non-empty and at most 65536 bytes, and a json-object document must fit the same 65536-byte limit.

Authentication

The adapter credential is a JSON access descriptor that is sealed on save and never returned. The CLI and the web form assemble it; the API takes it as the credential string.

ModeUsesSecret material
assume-roleThe node’s AWS identity assumes role_arn (optional external_id, session_seconds 900 to 3600).None
web-identityThe node’s projected token (AWS_WEB_IDENTITY_TOKEN_FILE, as on EKS with IRSA) assumes role_arn. The descriptor cannot name a token file.None
ambientThe node’s default AWS credential chain (environment, ECS task role, EC2 instance role).None
staticAn access key id and secret access key.The secret access key

ambient, assume-role, and web-identity act with the server’s own AWS identity, so they are refused until the node operator sets HIKYO_ADAPTER_AWS_WORKLOAD_IDENTITY=allow. Without that opt-in a project administrator cannot borrow the server’s IAM role. Scope the role’s trust and permissions accordingly, and prefer assume-role with an external_id per project.

hikyo adapter create --provider aws-secrets-manager \
  --origin https://secretsmanager.eu-west-1.amazonaws.com \
  --env prod --kind json-object --owner 123456789012 --secret prod/app/config \
  --aws-auth assume-role --aws-role-arn arn:aws:iam::123456789012:role/hikyo-sync \
  --aws-external-id hikyo-prod --names DATABASE_URL,API_TOKEN

For --aws-auth static, pass --aws-access-key-id; the secret access key is read like any adapter credential (no-echo prompt, --stdin, or --value-file). --aws-* flags also apply to hikyo adapter update --origin and hikyo adapter credential set.

The IAM policy needs, on the target secret ARNs: secretsmanager:DescribeSecret, CreateSecret, PutSecretValue, UpdateSecretVersionStage, TagResource, RestoreSecret, and DeleteSecret; plus secretsmanager:ListSecrets (which AWS does not scope to resources) for per-key planning and connection tests, sts:GetCallerIdentity, and kms:GenerateDataKey and kms:Decrypt on a customer KMS key. Do not grant GetSecretValue to the adapter’s role.

Ownership, drift, and deletion

The Hikyo ledger is the only ownership record. On the AWS side each owned secret carries the tag MANAGED_BY_HIKYO=<target id>, written when the secret is created (before its first value). Hikyo stages a value with HIKYO_PENDING, then conditionally moves AWSCURRENT from the exact observed predecessor using UpdateSecretVersionStage. Only after promotion does it move HIKYO_CURRENT and record the version in the HIKYO_VERSION tag. AWS automatically makes the first-ever version current, but conditional promotion still refuses a different version that became current concurrently.

  • Unowned names. A name that exists without Hikyo ownership is a conflict, never a silent overwrite. Adopt it from a plan like any other adapter; Hikyo then tags it before its first write.
  • External edits. If AWSCURRENT points at a version Hikyo did not write, the target reports a conflict and needs operator attention; the value is not overwritten. To let Hikyo replace that exact version, tag the secret HIKYO_VERSION=<that version id> (the conflict message names it) and resync. A later edit is not covered by that consent. A concurrent edit between inspection and promotion also refuses the update; the staged Hikyo value does not replace the external current value.
  • Retries. Each write carries an idempotency token that is identical for every retry of one outbox job, so replaying an ambiguous outcome cannot create a second version. A new job writes a new version.
  • Failures. Access denied, KMS denial, and expired credentials are auth; throttling is provider_limit and honours Retry-After; a timeout or 5xx after the request was sent is ambiguous and replays with the same token. Per-key targets report each failed name; the other keys still converge.
  • Deletion. Prune and teardown delete only ledger-owned names and always schedule deletion with the 30-day recovery window. If the same target needs a pruned name again inside that window, Hikyo restores it; a different target sees the old ownership tag and reports a conflict.

VPC endpoints and emulators

A VPC endpoint origin (or a local emulator) needs region in the descriptor (--aws-region). Private addresses need an entry in HIKYO_ADAPTER_EGRESS_POLICY_FILE. HTTPS is always required. The workload identity modes accept only recognized AWS Secrets Manager regional, FIPS, China, or VPC endpoint DNS names, with the endpoint region matching the access descriptor. An egress-policy entry does not authorize an arbitrary host to receive the node’s credentials. Emulators require static credentials. Workload identity modes always use AWS’s regional STS endpoint, because STS receives the node’s own identity material; only static credentials may name a different STS endpoint (sts_origin, --aws-sts-origin), for an emulator.

Vault/OpenBao KV v2

The vault-kv adapter delivers into a Vault or OpenBao KV version 2 mount. It is a destination only: it never reads a value back, and it is separate from hikyo import --from vault, which reads a tree into Hikyo once.

hikyo adapter create --provider vault-kv \
  --origin https://vault.example.com:8200[/NAMESPACE] \
  --env prod --mount secret --path apps/payments \
  --keys ID,ID --stdin < credential.json
  • Addressing. --origin names the server; append /NAMESPACE for a Vault Enterprise or OpenBao namespace. --mount is the KV v2 mount and --path the prefix under it (they fill the repository destination’s owner and name, so the API and browser take the same values). Each key becomes one secret at MOUNT/PATH/PREFIXNAME with a single value field. hikyo adapter target show --format workflow prints the path of every key. Secrets and config values share the tree, so names must be unique across both, case-insensitively. Values must be UTF-8 and at most 512 KiB. KV v1 mounts are refused: they have no check-and-set.

  • Credential. A bare token, or JSON: {"method":"approle","role_id":"…","secret_id":"…","mount":"approle"} or {"method":"token","token":"…"}. Either form accepts ca_pem (your CA; hostname is still verified) and spki_sha256 (base64 SHA-256 of the server leaf’s public key, checked in addition to chain verification, so pair it with ca_pem for a private CA). AppRole logs in once per delivery attempt, renews at most twice within it, and revokes its token afterwards. A static token is never revoked; its expiry is shown on the adapter. JWT/OIDC login is not supported.

  • Least privilege. The adapter needs no data read. A sufficient policy:

    path "secret/data/apps/payments/*"     { capabilities = ["create", "update"] }
    path "secret/delete/apps/payments/*"     { capabilities = ["update"] }
    path "secret/metadata/apps/payments/*" { capabilities = ["patch", "read"] }
    path "sys/internal/ui/mounts/secret"   { capabilities = ["read"] }
  • Ownership and conflicts. Hikyo writes with check-and-set and records managed_by_hikyo, hikyo_version, and hikyo_pending_version in each path’s custom metadata (names and version numbers only). A path that already exists and is not Hikyo’s is a conflict and is not touched until you adopt it by name; adoption never reads the value. A Hikyo path that someone edits outside Hikyo is also a conflict: Hikyo reports it and never overwrites it. If that key is later removed from the selection or the target is removed, Hikyo releases it with a warning instead of deleting the edit. An interrupted delivery replays from metadata, with a residual concurrency limit: if an external write takes exactly the pending version and the attempt stops before releasing custody, replay cannot distinguish that write from Hikyo’s own. A later retry can overwrite it; the external value remains in KV version history. See the Vault adapter ownership protocol before allowing concurrent external writers.

  • Retain, prune, destroy. Removing a target with --keep-remote leaves every path in place. Without it, and for keys that leave the selection, Hikyo soft-deletes the current version: history and metadata stay, and vault kv undelete recovers the value until the mount’s own retention drops it. Hikyo never destroys versions or deletes metadata; use Vault tooling for that after removal.

  • Versions. Every converge re-delivers every selected value, so each key gains a KV version per converge. Bound history with the mount’s max_versions.

  • Loops. A live hikyo import --from vault refuses to read a tree that an active vault-kv target of the same project writes (same host, namespace, mount, and overlapping path), so Hikyo never imports its own output.

  • Failures. A sealed or standby server and transport errors retry; a 429 honors Retry-After; a refused token or AppRole login marks the credential for replacement; a re-created or moved mount stops delivery until you reconfigure the target.

For a private Vault address, add an egress exception keyed by https://HOST:PORT (without the namespace).

Sealed webhook receivers

The sealed-webhook adapter pushes values to your own HTTPS service. Each value is encrypted to the receiver’s pinned age key and each request is signed by the Hikyo instance, so the values never appear in a URL, header, log, audit payload, or response. The receiver answers with a signed acknowledgement bound to the exact request. The wire contract is the sealed webhook protocol, and cmd/hikyo-receiver-example is a reference receiver.

Endpoints are instance configuration. A project cannot point Hikyo at an arbitrary URL. The instance administrator lists receivers in the file named by HIKYO_SEALED_WEBHOOK_FILE (see server configuration):

  1. The receiver operator runs hikyo-receiver-example keygen -dir DIR, which writes the receiver’s secret keys with mode 0600 and prints the public recipient_key and ack_key.
  2. The receiver operator runs hikyo-receiver-example fingerprint -origin https://recv.example -recipient age1... -ack-key ed25519:... -generation 1 and reads the result to the instance administrator over a separate, trusted channel.
  3. The administrator adds the endpoint with that value as confirmed_fingerprint and restarts. A missing or different fingerprint refuses startup; a changed host, port, or key changes the fingerprint.

Binding a project. A project administrator creates the adapter with the activated origin, the receiver namespace as --owner, and a binding credential the receiver operator issued for that namespace:

hikyo adapter create --provider sealed-webhook --origin https://recv.example \
  --env prod --kind organization --owner payments-prod --prefix "" --keys KEY_ID,KEY_ID

The binding credential is read like every adapter credential (no-echo terminal, --stdin, or --value-file) and travels only inside the encrypted payload. The connection test sends a signed probe and requires a valid acknowledgement.

Outcomes. A verified applied or already_applied acknowledgement advances the target. A missing, oversized, or malformed answer records the effect as unknown and the retry reuses the same idempotency key, so a receiver that already applied it answers already_applied. A forged acknowledgement (wrong key, bad signature, wrong request binding) fails the job and marks the target as needing attention. A receiver refuses to overwrite a name it did not receive from this target and answers conflict. Resolve it by releasing the name on the receiver or renaming with a prefix; hikyo adapter adopt is refused for sealed-webhook targets because the protocol has no adopt intent.

Rotation. To rotate the Hikyo signing key, pin the new public key on the receiver with the old key’s not_after at least a day ahead, replace signing_key_file, and restart. Rotating a receiver key or moving its host is a new trust boundary: bump generation and confirm the new fingerprint. Targets bound under the old fingerprint stop before sending anything. Remove them (pruning before you change the registry, or keeping the remote values and asking the receiver operator to release those names), then add them again. A receiver only lets the target that wrote a name change it.