Skip to main content

New packaging mechanism in 3.1.0

· 6 min read
Jeremy Scott
Co-founder

The new packaging system in 3.1.0 is here—and it finally makes multi-recipient, authenticated packages straightforward. You can ship a single package to many deployments, require multiple signatures (author, endorsers), and verify it all at package deploy time. The previous packaging flow still works if you need it.

Packaging authenticity is declared in bootstrap via packaging:

"packaging": {
"packageId": "my-app",
"packagerIdentities": ["packager-key"],
"requiredSigners": ["packageOwner"],
"additionalSigners": ["reviewer-1", "reviewer-2", "reviewer-3"],
"requiredAdditionalSignerCount": 2,
"requiredAdditionalSignerCount": 2
}

Only packageId is required; everything else is optional. Field intent:

  • packageId: simple equality check so you’re deploying the right package (non-versioned ID; keeps human intent clear).
  • requiredSigners: signatures you must see (e.g., the author, or a “major version lock” key). These are ids to keys in the bootstrap.keys
  • additionalSigners: a pool of allowed signers (e.g., staff reviewers). These are ids to keys in the bootstrap.keys
  • requiredAdditionalSignerCount: how many from that pool must sign (e.g., “any 2 of our reviewers”).
  • packagerIdentities: who is allowed to create the package (by identity key) so you know it was produced by an approved packager. These are ids to keys in the bootstrap.keys

All bootstrap properties are set during install and cannot be changed by opscotch at runtime.

What’s new vs old?

  • Old (still supported): command-line packager, single recipient, anonymous public-key encryption tied to bootstrap.agentPrivateKey (there is no way to verify the author), 1:1 package per deployment.
  • New (recommended): API-driven packager app (it's an opscotch app, not a standalone CLI), authenticated public-key encryption, multiple recipients in one package, and multiple required/optional signatures. This means you can build once and deploy to many recipients while enforcing authorship authenticity and endorsements.

Authenticated encryption and multiple recipients

Packages are encrypted with authenticated public-key encryption (X25519 + XSalsa20 + Poly1305), so the recipient knows who encrypted it and only authorized recipients can decrypt. You can list multiple recipient keys, so one package serves multiple deployments.

Signatures and endorsements

  • Put the package owner's key in requiredSigners - this ensures that only packages truely created by the owner are deployed.
  • The same mechanism can be used to create a “major version” key to enforce version-lock signing across a major line - author also signs with the matching major-version-key, and your bootstrap asserts that only that major version line is deployed.
  • Use additionalSigners + requiredAdditionalSignerCount to say, “any 2 of these reviewers must sign.”
  • Signatures are checked at deployment; missing or mismatched signatures block install.

Packager app (API-driven) quick flow

  1. Get a hash from the workflow config (use this to check the correct sources are being used):
curl "http://localhost:39575/workflow-hash" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '@/path/to/workflow.config.json'

Example response:

{
"hash": "3EFD9FF10F931917FE0710D1F9B503D59ECF741B6C07A8A86E72C8A0DA51F072"
}
  1. Get a compiled JSON version of the workflow with all the resources loaded into script tags (use this to diff for version control etc):
curl "http://localhost:39575/workflow-compile" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '@/path/to/workflow.config.json'
  1. Generate any keys that you don't already have, use sign for signatures, and authenticated for packaging identities:
curl "localhost:39575/key-gen?purpose=sign"

Example response:

{"purpose":"sign","publicKey":"E07F3F3ECFDF3C34E9E423CA007C869D1E336467F5E774F4150ACC8B583C41A8","secretKey":"546CAA7DB480BB6833437A39F35DDF882E84E7B430332A4D7C7A78013F4D510EE07F3F3ECFDF3C34E9E423CA007C869D1E336467F5E774F4150ACC8B583C41A8"}
  1. Dry-run package with signatures and recipients (use this to check metadata etc):
curl "http://localhost:39575/workflow-package?dryrun=true" \
-H "Accept: application/json" \
-F 'body={
"packageId": "my-app",
"signers": [ { "key": "owner-key", "signText": "signed by owner" } ],
"encryptionKeys": [ "recipient-1", "recipient-2" ],
"packagerIdentityPrivateKey": "packager-app",
"keys": [ ...your public/secret keys here... ]
}' \
-F 'stream=@/path/to/workflow.config.json;type=application/octet-stream'

Example response:

{
"manifest": {
"envelopes": [
{
"type": "workflow",
"signatures": [
{
"id": "signed-by-opscotch-packager-app",
"signText": "aaa from bbb",
"signature": "C67DE57158DE8E11A7F0823BA7B102AA942557222A2208CF836315201574C502AE5DBA63EA949296CF1E804B14DC4A980E71EF4B19E63BF7D3BF429CD9259007",
"date": "2025-11-24T02:07:18.231Z",
"hash": "58FC76195565C9327E26876ECCA21CD0364C280102586E82884905D1B58D6661"
},
{
"id": "user1signx",
"signText": "aaa from ccc",
"signature": "F9C20A3B1D811550E5A9A7EF9B247B1FA0E4841B598168169B2A8AD8071536D77ACEF29BC32BCECA9094664039FB55F50542082558005B9DEBF0EBB8AA33EC05",
"date": "2025-11-24T02:07:18.231Z",
"hash": "2B9B14247400F162D931E8E8752047B7F8B6AC6E9D4A4A2D1B1B651E941D90FF"
},
{
"id": "user2signx",
"signText": "aaa from ddd",
"signature": "EEF6DC604CA56A83B0CCAEE2DCD68FF399BEF00B0F0D769AFC771E2F050D13D95989D0385A3981271EBC6E1091A7244B386FC813C29F7ECA777633A8319C8900",
"date": "2025-11-24T02:07:18.231Z",
"hash": "CC87CB9492FA9B05A461D2B289FBF7BB0462DBD9B47E4E588546C52BAF42917F"
},
{
"id": "user3signx",
"signText": "eee from bbb",
"signature": "9B03462F7EB8EEF5A74BCA9B1C60CC77573CC3651236FDB14B9F14E8FE7BFFF42EC989954F2076CC81A84D231952CB87DDEDD9A89E219C2A3DC0DD3FDF0D2D0D",
"date": "2025-11-24T02:07:18.231Z",
"hash": "D74D2EB107499BF0BB5BEC1CCD19D62B9AC06BCB140FE6BB370EEAF782A3B205"
},
{
"id": "user4signx",
"signText": "fff from bbb",
"signature": "2D16FD271B8F059CC17C18651A4D9949DCFBB6B529DC5DAC0D792BEA1A77905453BA1E727330CAE8446F597993B489E49AE85E53261F56BB0C3E985AD2BC9F06",
"date": "2025-11-24T02:07:18.231Z",
"hash": "59F46F2C3F4CDE4FF26278BF218B2EEC7BCE9C42E95DC5E20BB04425CE8E23A5"
}
],
"payloadHash": "6090F88322E2F9AF915677F25D1563641B85B2A744AC3CE22F203E2BFF39180A",
"includesLicense": true,
"licenseSteps": {
"value": "250",
"properties": {},
"changeBy": "D0CF0414629EF5E90EA5",
"delimiter": ":"
},
"payloadSize": 12324,
"payloadPeek": "010001183BE9FDBEE0FCCEFDE055C2C23E2CAE785C0F0ECE214508E1022C2F219370AB8D854503A3E01FE0EC6461455F952B",
"licenseSize": 2603,
"licensePeek": "00011431413842304642414344433941353039443743310118085E0B6FEE62C65B00F92CBE69813D7942C100ED80EAB45F01",
"totalSize": 14927
}
],
"workflowHash": "3EFD9FF10F931917FE0710D1F9B503D59ECF741B6C07A8A86E72C8A0DA51F072",
"packageId": "opscotch-packager",
"packageType": "workflow",
"stepCount": 7,
"minimumAgentVersion": null
},
"payloadSize": 12324,
"licenseSize": 2603,
"usedEncryptionKeys": [
"user-1",
"user-2"
]
}
  1. Produce the package file
curl "http://localhost:39575/workflow-package?dryrun=true" \
-H "Accept: application/json" \
-F 'body={
"packageId": "my-app",
"signers": [ { "key": "owner-key", "signText": "signed by owner" }, { "key": "major-version-5-key", "signText": "this is version 5.x of the app" } ],
"encryptionKeys": [ "recipient-1", "recipient-2" ],
"packagerIdentityPrivateKey": "owner-key",
"keys": [ ...your public/secret keys here... ]
}' \
-F 'stream=@/path/to/workflow.config.json;type=application/octet-stream' \
> my-app.app
  1. Envelope the package with additional signatures:
curl "http://localhost:39575/package-envelope" \
-H "Accept: application/octet-stream"
-F 'body={
"packageId" : "my-endorced-app",
"signers" : [ {"key": "endorser1", "signText" : "endorsed by bob from our-org"} ],
"encryptionKeys" : [ ... ],
"packagerIdentityPrivateKey" : "our-org-key",
"keys" : [ ... ]
}'
-F 'stream=@my-app.app;type=application/octet-stream' \
> my-app-endorsed-by-our-org.app

  1. Inspect the package with:
curl "http://localhost:39575/workflow-inspect" \
-H "Content-Type: application/octet-stream" \
-H "Accept: application/json" \
--data-binary '@my-app-endorsed-by-our-org.app'

Bootstrap on loading agent example (minimal)

"packaging": {
"packageId": "my-app"
}

Bootstrap on loading agent example (auth + endorsements)

"packaging": {
"packageId": "my-app",
"requiredSigners": ["owner-key", "major-version-5-key"],
"additionalSigners": ["reviewer-a", "reviewer-b", "reviewer-c"],
"requiredAdditionalSignerCount": 2,
"packagerIdentities": ["owner-key", "our-org-key"]
}

This says:

  • must have author + major-version signatures (guarentees that this is version 5.x of the app), plus any 2 of the 3 reviewers, and the package must be built by the approved packager identities: the owner and the org.

Why this matters

  • Ship once to multiple deployments (multiple recipients) with authenticated encryption.
  • Enforce authorship and endorsements with required + threshold signatures.
  • Keep packages human-comprehensible with a stable packageId.
  • API-driven packager app instead of a one-off CLI.
  • Old packaging still works if you need backward compatibility.