diff --git a/.prometheus/decisions.md b/.prometheus/decisions.md index 905a976..393d93f 100644 --- a/.prometheus/decisions.md +++ b/.prometheus/decisions.md @@ -290,3 +290,49 @@ that the `compass` pin must be a tag on a clean commit rather than the dirty sid **Decision.** Added `aarch64-pc-windows-msvc` to `dist-workspace.toml` targets with a native `windows-11-arm` runner. Branch `feat/windows-arm64-target`, commit `3a8d182`, unpushed. **Why.** the-boss ships `electron-builder --win --x64 --arm64`. The target list covered `x86_64-pc-windows-msvc` only, so an arm64 Windows build would have had no artifact to download. The sibling compass fork already builds both Windows arches. No workflow regeneration is needed: `release.yml` builds its matrix at runtime from `dist plan` and carries no config-freshness gate — verified by reading the generated file. `dist` is not installed locally, so CI is what confirms the new job appears. + +## 2026-09-30 — The Boss consumes the latest committed liter-llm fork + +**Operator instruction:** update liter-llm and prometheus-skills-mini submodule links to +the latest fork revisions for the next The Boss build. The mini baseline is +`ac99730e2b7b5cb22079d984abc0fb92fb911aff` from +`Prometheus-AGS/prometheus-skills-mini/main`; its 100 skills and executable dependencies +remain intact. The accepted liter-llm source is +`GQAdonis/liter-llm@12a2fae9675e34b88e9373caa2bca9959f416493`, fetched from the fork's +`main` on 2026-09-30. This supersedes the `4f25d39f0075656b70bf945880d6033beda6d663` +gitlink for this explicitly requested release payload. + +**Authority remains visible:** `versions.toml` is operator-authored and is not edited +by this update. Its liter-llm line still records the prior `4f25d39...` baseline; +the explicit instruction and exact override above explain the discrepancy. The +checked-in gitlink and The Boss integration/payload revision manifests record the +consumed revision. No other dependency or SurrealDB baseline changes here. + +The new source retains liter-llm version 2.1.1, preserves `schemas/providers.json`, +and updates `schemas/catalog.json`. The Boss must pin the new catalog checksum and +the same fork revision used by its UAR payload. Source selection and copied skills +are not compilation, inference or installer acceptance evidence; those belong to +the completed application build and actual operation boundary. + +### Same release — preserve UAR's explicit host transport boundary + +The actual UAR release compilation against `12a2fae...` exposed removed +`redact_base_url` and proxy/redirect policy APIs used by its credential boundary. +The fork repair `GQAdonis/liter-llm@1bacb4adc47ba9fa262e1edec6c2f32c4f4649ba` +on `codex/uar-host-transport-boundary` restores that explicit compatibility; +PR: https://github.com/GQAdonis/liter-llm/pull/2. This exact source supersedes the +`12a2fae...` selection above for both the UAR and mini consumers in this release. +Its catalog bytes are unchanged from `12a2fae...`. Operator-owned `versions.toml` +remains untouched and this accepted source override remains explicit. + +Previously published native CLI artifacts retain their actual source revision +until replacement artifacts are built and published. Advancing this source +gitlink must not relabel those existing artifacts as compiled from the repair. +The observed compilation failure is the reason for the compatibility update; +this mini change does not claim a successful rebuilt application or operation. + +The native release build then exposed one `ClientConfig` builder constructor +missing the restored transport-policy field. The same fork PR's final repair +`0617979022aea621dd13541dee07ad84ffcf7d21` initializes that field and supersedes +`1bacb4a...` as the exact mini/UAR source selection. Skill and catalog bytes are +unchanged; their payload is regenerated by the next actual application build. diff --git a/docs/agent-team-creator-c094-distribution.json b/docs/agent-team-creator-c094-distribution.json new file mode 100644 index 0000000..10a0595 --- /dev/null +++ b/docs/agent-team-creator-c094-distribution.json @@ -0,0 +1,847 @@ +{ + "schema": "prometheus-mini.shared-skill-distribution/1", + "change": "afc-c09-team-execution-cooperation", + "source": { + "repository": "Prometheus-AGS/prometheus-skill-pack", + "commit": "1ddcc8b21b05f26aa89aa5e19776c86e5b854c96" + }, + "miniBaseline": "c347043dc046f6da88b7e938f273e5152c43e76f", + "uarSource": "0db89a46db5bd590e5d9a9732389f41e3785bdb1", + "preservedLiterLlmRevision": "0617979022aea621dd13541dee07ad84ffcf7d21", + "status": "source-distribution-not-installed-runtime-conformance", + "excludedDirectories": [ + "node_modules", + ".git" + ], + "payloads": [ + { + "name": "agent-team-creator", + "sourceDirectory": "skills/process/agent-team-creator", + "destinationDirectory": "skills/agent-team-creator", + "files": [ + { + "path": "agents/openai.yaml", + "size": 209, + "sha256": "6605ed41446890050e2b32534d10ffa2e3d99f5abc7bcb4f810991b9a42d46d1" + }, + { + "path": "assets/deploy-intake.json", + "size": 276, + "sha256": "7846c47e64d20da15b1f58035f69bf278ba9c4bbe7ab164e0ef95372f1d20f30" + }, + { + "path": "assets/intake.json", + "size": 400, + "sha256": "3504c2b28099192eea6857465be7f2fc91bffffb31b7e869c1f339fe113bc97f" + }, + { + "path": "assets/revise-intake.json", + "size": 148, + "sha256": "aa1cfe2a7cbce5faefb9da4a147fa6bedf28014d8adda1fa04e0df536159c973" + }, + { + "path": "assets/uar-intake.json", + "size": 439, + "sha256": "d09e6c8dd9c7ce413b2c9756ad24fd6268774c95694a6611b987f31fea176f9e" + }, + { + "path": "assets/uar-package.json", + "size": 11413, + "sha256": "d13f0d3f4290374e173053cbea24cfdfa95accb3be27c5ab165d54443771ba9e" + }, + { + "path": "assets/uar-workspace/agents/child.json", + "size": 3729, + "sha256": "bbe71a2309c773bcc8b60972457b23616dcad126135b25482e2e7a707579b8da" + }, + { + "path": "assets/uar-workspace/agents/coordinator.json", + "size": 3862, + "sha256": "091b8e1ecd4f8f0801d3bb1ce505fd6ad46684d5b2b31a5a2290ac62504dd404" + }, + { + "path": "assets/uar-workspace/agents/specialist.json", + "size": 3759, + "sha256": "64b9d0cc68825b03febd4bb6865e1b9e8ea8032d47c812385bf720aa364dda50" + }, + { + "path": "assets/uar-workspace/manifest.source.json", + "size": 677, + "sha256": "ae009045ddcac16d331d727a8b1b312702af44549f5d27609326c4281fd43a31" + }, + { + "path": "assets/uar-workspace/teams/root.json", + "size": 2327, + "sha256": "52c8c6ecd93dba0375b19cea94844d7dc9308bf90dc2e23f44bcd0fdcbfb37f8" + }, + { + "path": "assets/uar-workspace/teams/subteam.json", + "size": 1754, + "sha256": "9573b49328b3643098f253e84873d97577e1c21fe7c8869cfbc3a408f74d594c" + }, + { + "path": "assets/uar-workspace/workflows/review.json", + "size": 2240, + "sha256": "1a5a168434ca6f0462e2c601bd39e052baa69e53514cba6c7d54a27ef66e5c2a" + }, + { + "path": "assets/uar-workspace/workflows/specialist.json", + "size": 1582, + "sha256": "7b89846ff540e86b01d452a0fd6a585cc2784c040ac553a97c38d18f2196b9c3" + }, + { + "path": "assets/uar-workspace/workspace.json", + "size": 19635, + "sha256": "a6fed884060958000b1b362a908d22534e66664e869d5c15f17bd06143b5bd38" + }, + { + "path": "references/manifest.md", + "size": 9287, + "sha256": "be7ac9e3b60626d60afa3883c32c0937d2e9dd0b4de9df4e6438d19e86ef1014" + }, + { + "path": "references/models-memory.md", + "size": 9621, + "sha256": "948d23f9c3596899f77405590b9f521fa0428cc1aebc7984604a1b37e80cabec" + }, + { + "path": "references/native-harnesses.md", + "size": 10546, + "sha256": "95a3bf751875b379e0fe7494835669677cf584cf2d0f88e78601f4d8a6a01eee" + }, + { + "path": "references/project-installation.md", + "size": 4544, + "sha256": "11362d423ddb1575d1593fa11c4f563e29174614c7e9d4a2f6a1fd18e0f51086" + }, + { + "path": "references/task-handoff.md", + "size": 21479, + "sha256": "9eacc067f5684d9318e5809083bd378fda4b7447a0a7f19f203fb4951952cfee" + }, + { + "path": "references/uar-deployment.md", + "size": 8426, + "sha256": "bb92c74c21ea8c55dfb29462796c2402d17669078cdba0172c9bc305e7d702d6" + }, + { + "path": "runtime/.gitignore", + "size": 14, + "sha256": "4d56952b0fb13bf8f9b6c13a6d4c34a075bac3af447636a1df4335d7576e2f97" + }, + { + "path": "runtime/package-lock.json", + "size": 14286, + "sha256": "3c77361d286ccc6a2ccc70b868e9fe13071bc2baf3c08dcb655a8ac7ceb9b823" + }, + { + "path": "runtime/package.json", + "size": 382, + "sha256": "0f26c4c08d6c2c24d4e68fcc481145ea51e39679ddf0443de6f96530accd2f91" + }, + { + "path": "runtime/src/adapters-codecs.mts", + "size": 3158, + "sha256": "d71fc1823b782a2bb15aa71f6ebda1ad1a3862888baa95a29f0b30222cd6312b" + }, + { + "path": "runtime/src/adapters-local.mts", + "size": 9349, + "sha256": "bb1bc924acc6e32ade0f59798ff3fc3b1a6cd5bbebe615966b1fa4d10d0b11b4" + }, + { + "path": "runtime/src/adapters-services.mts", + "size": 5703, + "sha256": "2d70127f347efddeb9b30436576612dc22d4798e4f208aea72f817b19174f0e7" + }, + { + "path": "runtime/src/adapters.mts", + "size": 5749, + "sha256": "877399e6e9ea7f22112a6b175d666fe77648c77dcf05c23d1cedf11bce2a3d11" + }, + { + "path": "runtime/src/cli.mts", + "size": 7309, + "sha256": "d7bb19d1f906c948ab9de776f27906b469273b0c8d3030cbf35238bc346b1f84" + }, + { + "path": "runtime/src/export-files.mts", + "size": 1911, + "sha256": "36ef91333e7d4b5216826d11ae344e7a87d1497262dfbe0973332563e4afe6f4" + }, + { + "path": "runtime/src/guidance.mts", + "size": 16859, + "sha256": "0a98d01cef86d96a80af294a0389212fa188ddd2933e3b47e2ec266a3737904d" + }, + { + "path": "runtime/src/handoff.mts", + "size": 5447, + "sha256": "621ebe0dc0ab197da7e70a121953ed6dd2bf47ca4f3cf35c367aa38dcdaa09e2" + }, + { + "path": "runtime/src/memory.mts", + "size": 9559, + "sha256": "183a0e80864919c4ab8e16a75aa3774ffbb2e0114780d6ae257562eeebf6fea4" + }, + { + "path": "runtime/src/models-http.mts", + "size": 5160, + "sha256": "1c71810b84836375f076e4d3aafd3493b1d22ea86e8675588526ca63ff012e27" + }, + { + "path": "runtime/src/models.mts", + "size": 12375, + "sha256": "ff8a4366a637eb2a02ddbd0b2dd9e7f40411eaba9120da1069a405450f812ec1" + }, + { + "path": "runtime/src/project-files.mts", + "size": 4315, + "sha256": "a171c5957f3969af12be2c614a0c8f0797072fccd437bd5c526127e9a56ed8b6" + }, + { + "path": "runtime/src/project-install.mts", + "size": 7558, + "sha256": "6708bc02f33380ec3e711e0c183e8169a30eae3a0799d5a3668719506b08d89c" + }, + { + "path": "runtime/src/project-instructions.mts", + "size": 2826, + "sha256": "037fb6cfbd17dd6417e002c5ab7f7c13db7b731623fb2295a0f908f51eabcfb8" + }, + { + "path": "runtime/src/state-kbd.mts", + "size": 5227, + "sha256": "653614a24dcb49b39089a70c96278393d78a7eef692ca4dae6623dbce4f5a2ee" + }, + { + "path": "runtime/src/state-tasks.mts", + "size": 5719, + "sha256": "97a0b2986b597636e3a9f1855f0aec88fa9fdfa818404d690d6a40e338e449b2" + }, + { + "path": "runtime/src/state-validation.mts", + "size": 9564, + "sha256": "b1c89ee09c226e6c38c0f3134a5111a7d9724b75f57ae12a047bfcd8c32a72a6" + }, + { + "path": "runtime/src/state.mts", + "size": 4781, + "sha256": "51f9b29c50ca14c8ce6af539c203390f44d6215b7c98fb312abd4cee4dacd797" + }, + { + "path": "runtime/src/types.mts", + "size": 5449, + "sha256": "4a5a63aa9413a8cf911a2f5d79a42da7084dd1c7d08fddbaa73febbcda646a2c" + }, + { + "path": "runtime/src/uar-client.mts", + "size": 7437, + "sha256": "58bd60466824b926154a5d6b198ed7b2daea04a860e06b00fbd7fd022b8eff9c" + }, + { + "path": "runtime/src/uar-package/binding.mts", + "size": 2274, + "sha256": "f2fb495307f4b364eedd28763f4a3c01ccbd851eeb30b794a292f8fd9c23701b" + }, + { + "path": "runtime/src/uar-package/canonical.mts", + "size": 2438, + "sha256": "597e5ed9a95ed4e3e9070d2d02d7532bd86502653b0968f2e2c2adee0dfcfa16" + }, + { + "path": "runtime/src/uar-package/commands.mts", + "size": 3309, + "sha256": "907e6ad0eecd2d894ba043e9ef08eb396bf6675bf606a9bd7622cb6e0902b88e" + }, + { + "path": "runtime/src/uar-package/compiler.mts", + "size": 10644, + "sha256": "7685d088e76ec39adb558a380d69fd7f77cd0955780072b8522de45934554706" + }, + { + "path": "runtime/src/uar-package/graph-validation.mts", + "size": 9411, + "sha256": "ace0311f72469e2ecef1755e926e986f386d0da10cfd83313c5131b0fec1d405" + }, + { + "path": "runtime/src/uar-package/maintenance.mts", + "size": 2747, + "sha256": "fa77b391725348fbf01da81c8b15f129c83de501998c4e866ce623538099aa0d" + }, + { + "path": "runtime/src/uar-package/migration.mts", + "size": 15146, + "sha256": "aea464b10e2b5a6aab88a0a14f24bda09ab4dacd800186208b288d5049c9f2f2" + }, + { + "path": "runtime/src/uar-package/package-files.mts", + "size": 3697, + "sha256": "9578b0150a8c2f6598b1c4f027677f98e6b91156a5f4d45ba8728407fdedb169" + }, + { + "path": "runtime/src/uar-package/private-authority.mts", + "size": 3768, + "sha256": "6e048d4aea147a809a3b95686cbd0b60defadf4785efe38efcdd63401b4b0af6" + }, + { + "path": "runtime/src/uar-package/profile-validation.mts", + "size": 7565, + "sha256": "ff3adcf899321c7afb87179796f915ac38be778344540663d239d5245d183984" + }, + { + "path": "runtime/src/uar-package/workspace-questions.mts", + "size": 5587, + "sha256": "a52fcb8dbfd879fcc4b3d66382d0adefc7550e13f9d5867534e672754faaf0ca" + }, + { + "path": "runtime/src/uar-package/workspace.mts", + "size": 24928, + "sha256": "426ca9de7972dc2878d9dbb3668731b2e88cbb4ab68b979e0344dff532f88121" + }, + { + "path": "runtime/src/uar-package.mts", + "size": 650, + "sha256": "4ec40be5e4ab5ae8a5b19e92ef4d753504cb98a70e0bb28cb3e6afd5dafc4b07" + }, + { + "path": "runtime/src/ui-bindings.mts", + "size": 2084, + "sha256": "394f67a00ab4496541abcb08a97380e0be6399f316726beda4dcb1e9558bd2e2" + }, + { + "path": "runtime/src/validation.mts", + "size": 5438, + "sha256": "86c67c85760ef2a6b0ebda4016e22e6a8b6c0a1415c1f60dda181849185046b2" + }, + { + "path": "runtime/test-src/export.integration.mts", + "size": 8255, + "sha256": "67966d9ecf9e23476a092bd1ff91bf7cbefb5f79b42eb2db06da3bc74cf3e5ed" + }, + { + "path": "runtime/test-src/fixture.mts", + "size": 2465, + "sha256": "ee2911b7ec3bc832ee6372ff3064e96fab77e2f1836035c35453ee9558eb734a" + }, + { + "path": "runtime/test-src/kbd.integration.mts", + "size": 9820, + "sha256": "af46a51cb71bf8ea300e18c9a534941703934656fa3b9246dcc56a87bef796a5" + }, + { + "path": "runtime/test-src/models-memory.integration.mts", + "size": 16025, + "sha256": "8ab207b37cbbf32d7f25da6a52901fb761377401c82d418aafeab3988d4ef7f7" + }, + { + "path": "runtime/test-src/state.integration.mts", + "size": 14291, + "sha256": "929719ff2a562a6712fcabdb6dd715c64985a650ac497fee5e00406054b9e795" + }, + { + "path": "runtime/tsconfig.json", + "size": 313, + "sha256": "2e1e208abd20ca4c26a103e199feb49bfd39e21eeff32a52575af9c9853bc322" + }, + { + "path": "runtime/tsconfig.tests.json", + "size": 159, + "sha256": "fcb187289ca8003900d8ac1acfbc947de8afbd3b424987482873dd6edaffb060" + }, + { + "path": "schemas/project-routing.schema.json", + "size": 993, + "sha256": "a868133067fe40154e0226c180c5c2e6d77d23a304c7739bd0ab9ca044633471" + }, + { + "path": "schemas/team.schema.json", + "size": 2808, + "sha256": "76ccd7ef710ee72f60ae89480167fa0129da9f795a103849b2315595d2ff59e9" + }, + { + "path": "schemas/uar/0.1.0-draft.2/agent-definition.schema.json", + "size": 3504, + "sha256": "a5e0573b5323accec7e25766287af897cee34f689d77a2b6eb9d9367d41f179c" + }, + { + "path": "schemas/uar/0.1.0-draft.2/collaboration-document.schema.json", + "size": 809, + "sha256": "e7f8215d6e177f3ab00374df0ab34d0f19e24552af34c69aaf5b049260ccc2d1" + }, + { + "path": "schemas/uar/0.1.0-draft.2/common.schema.json", + "size": 10971, + "sha256": "a832c0018f11abf564b341cc0ba9ed6d87172968fe2f43bdbcb0b4fb6fcfc2ef" + }, + { + "path": "schemas/uar/0.1.0-draft.2/consumer-source-receipt.json", + "size": 2407, + "sha256": "db59f5a6be46253d84221c8e4677e6812b0ecba5a1681c482ff8c98d313184bd" + }, + { + "path": "schemas/uar/0.1.0-draft.2/conversion-report.schema.json", + "size": 2033, + "sha256": "4a013291e1768a5f0b98f6c99d3ec746f6a863cfd55d50d73863161943fb82cb" + }, + { + "path": "schemas/uar/0.1.0-draft.2/deployment-binding-template.schema.json", + "size": 2887, + "sha256": "73432b4f73217b166d13013cfdda63f0b139dd191196f009fc7e629886a2c22b" + }, + { + "path": "schemas/uar/0.1.0-draft.2/deployment-binding.schema.json", + "size": 6740, + "sha256": "44f91ff1ebd6f6b31bd4d60dd1a928f5969033f2e218dec5d3eb36d111ad15f9" + }, + { + "path": "schemas/uar/0.1.0-draft.2/effective-binding-receipt.schema.json", + "size": 3982, + "sha256": "b19661c1f662e2133cd884b0104dfe0e8330f132ced8064b5c6ff89bf8aa01da" + }, + { + "path": "schemas/uar/0.1.0-draft.2/package-manifest.schema.json", + "size": 3317, + "sha256": "bb96cd181790f391f9a543799f93369f8c53125ba9b3f70355f5f4c06fc1bd71" + }, + { + "path": "schemas/uar/0.1.0-draft.2/representation-grant.schema.json", + "size": 5455, + "sha256": "b856385bb5d1ab40fdb1e7e28d62ee0c9a0843e2fe46dd9162224d70c7ed8b01" + }, + { + "path": "schemas/uar/0.1.0-draft.2/team-definition.schema.json", + "size": 5076, + "sha256": "3b7f1102c5d822b654dc562689b7fa6316c80c01b63d3bb3fe808d4ee30673ef" + }, + { + "path": "schemas/uar/0.1.0-draft.2/workflow-definition.schema.json", + "size": 3845, + "sha256": "6129578c9c54d5fd7134ea11ef149ba1d629841b205856f6b151be6ff03a9abb" + }, + { + "path": "schemas/uar/agent-definition.schema.json", + "size": 2898, + "sha256": "baddf2dea8f9f3ca5ab2db149258c63e0c38fddca82d39d874a76df4345d59ae" + }, + { + "path": "schemas/uar/common.schema.json", + "size": 6841, + "sha256": "4b927640a4edc605697a56318ee028f6389d67a0e561aaa0f988c4519174cd0b" + }, + { + "path": "schemas/uar/deployment-binding.schema.json", + "size": 4436, + "sha256": "e757c155db34b44fe7e9b98910e32340d54a21001c556acc74aa293bcb7cb039" + }, + { + "path": "schemas/uar/package-manifest.schema.json", + "size": 3326, + "sha256": "fc5670fa6eace7abb777edded9a7bd6dffbe63c6410d581fd991db22587426f3" + }, + { + "path": "schemas/uar/README.md", + "size": 1089, + "sha256": "d654fd2ee04de8582942095caca43632572cbf3e870a9521a04535f02ba8c64b" + }, + { + "path": "schemas/uar/team-definition.schema.json", + "size": 4466, + "sha256": "8e613a1597507a94dd2b4697384234464f80682992e4c2e78bddc448b6a3b099" + }, + { + "path": "schemas/uar/workflow-definition.schema.json", + "size": 3854, + "sha256": "8dee4d2b0fc5a8396cde3ad7c13c8484dcb7d758b847488aa308d92735334355" + }, + { + "path": "schemas/uar-migration-diagnostics.schema.json", + "size": 1368, + "sha256": "74279916116e19ddac325ef11425249d817a5e15a75e86c2b02047111358710f" + }, + { + "path": "schemas/uar-package-authoring-v2.schema.json", + "size": 1467, + "sha256": "ef8870b61c066c8f871c91570b8e7d5d340053ddc6ad54491782b2a2130683f6" + }, + { + "path": "schemas/uar-package-authoring.schema.json", + "size": 1648, + "sha256": "d0d4ca9cdb112e86528dc88745e5d754a0f40bbc599b796c23000644467c5e40" + }, + { + "path": "schemas/uar-workspace.schema.json", + "size": 2579, + "sha256": "2ea9f4f2696d9c609faa791e92ea36c3184cc3958fa5006a2c978975f7464997" + }, + { + "path": "scripts/adapters-codecs.mjs", + "size": 2755, + "sha256": "38c3b9e0462b36476726cac75ca4ce7abe6010e391d9d59658c5d949b9c515f8" + }, + { + "path": "scripts/adapters-local.mjs", + "size": 9883, + "sha256": "7aaed09c709a78ee8e42a6975bf6ae4404772284f10d42afe770a2bbffa1a227" + }, + { + "path": "scripts/adapters-services.mjs", + "size": 5812, + "sha256": "117a6bae4337fb6b1ab8dffe32f5462db590d5640caf09dc35bc2626f55ae2c6" + }, + { + "path": "scripts/adapters.mjs", + "size": 5793, + "sha256": "ec2a6d7eb576676117fef3cc914e9802955bdcbfe318cbfa7264a37bb76d2085" + }, + { + "path": "scripts/cli.mjs", + "size": 7697, + "sha256": "ff88099f95da61202b7f1fa09fff71fe104e6b174084f05505901538eb6ba104" + }, + { + "path": "scripts/export-files.mjs", + "size": 1911, + "sha256": "8f193b8cc9190c5ba4b83c4c37eb0757e34438d9920737f5f852e6ce0143a196" + }, + { + "path": "scripts/guidance.mjs", + "size": 16939, + "sha256": "9338adfef21690ee6e260d2dc1c97d7772b28208f97a70745324d7bbe32de756" + }, + { + "path": "scripts/handoff.mjs", + "size": 5444, + "sha256": "1510e7cbcac0e685dd155fca0369fd08d20163b0d8aa104dc846da8e836c14ae" + }, + { + "path": "scripts/memory.mjs", + "size": 9507, + "sha256": "5d86cb097580a1d018effe7e27075574610762d6ca338c5eeec7644e7be53e78" + }, + { + "path": "scripts/models-http.mjs", + "size": 5440, + "sha256": "aea304aa515f8468446094ef3c5e140c78cffdbe63d4b54d414e3d1de709dc28" + }, + { + "path": "scripts/models.mjs", + "size": 12402, + "sha256": "8fbb72e7815e4f3e23d535679fae8a2deb73f013dfd40466952acf95d70f7b22" + }, + { + "path": "scripts/project-files.mjs", + "size": 4524, + "sha256": "607a5d9bd8b44ff99286960a3a0137d1f6b385ce0aff6191cebe78349bf5e8d0" + }, + { + "path": "scripts/project-install.mjs", + "size": 7460, + "sha256": "b3a0d61108d3a30f603c6670d913bf5259ed84ed06f5daaf020fe1341f220f91" + }, + { + "path": "scripts/project-instructions.mjs", + "size": 2818, + "sha256": "45736cc6602bc4bd971418ea0ad49a0dff479c848bf4b63520f094473e4b4f6c" + }, + { + "path": "scripts/state-kbd.mjs", + "size": 5215, + "sha256": "d6083c558b54b994553208d4cbcfe221c71a48e7a5edbee03da730e3c6be5aad" + }, + { + "path": "scripts/state-tasks.mjs", + "size": 6118, + "sha256": "3ef330a0f3689da1bd74f8a1998f51877c2e4f0d79d0b1fd23ff3b593504120d" + }, + { + "path": "scripts/state-validation.mjs", + "size": 10077, + "sha256": "da6086611c376daf42472aa2548b5874d622917d4ecd8d16077b31c1a563cc9e" + }, + { + "path": "scripts/state.mjs", + "size": 4837, + "sha256": "90ef7e07aae91ef787245d384fdac1c5e99fc676fa40db7d34bc1044257c9928" + }, + { + "path": "scripts/types.mjs", + "size": 267, + "sha256": "a797bafbb091a2fb00a7ee5aff31badca6c8fc76f1c82eca0bdffb539c44bfb5" + }, + { + "path": "scripts/uar-client.mjs", + "size": 7011, + "sha256": "13f0b36e40e549ef4b304afca5cedb2b3fb10ed87830d896eadf7836c62891b4" + }, + { + "path": "scripts/uar-package/binding.mjs", + "size": 2302, + "sha256": "4b3c8839da16d7547a36e4f4d5baadc6fe8b5480c42f29b08934325cdd018ca7" + }, + { + "path": "scripts/uar-package/canonical.mjs", + "size": 2430, + "sha256": "0f05afbd7c9d9e65146924f7de825448871966230659d3598129441bb2bf66fd" + }, + { + "path": "scripts/uar-package/commands.mjs", + "size": 3258, + "sha256": "990daaef6407273d381fd5d07360b5911e3a4e36c61fcc894100006c903ddc3b" + }, + { + "path": "scripts/uar-package/compiler.mjs", + "size": 10635, + "sha256": "095bcf71a455f191361cae3d6927dc51d7061d3b86d65b6b7db095fdc158254d" + }, + { + "path": "scripts/uar-package/graph-validation.mjs", + "size": 9892, + "sha256": "da13627077cb125edcf7e7f96bafd62c7a504d69f445335bd11e0946b3169c05" + }, + { + "path": "scripts/uar-package/maintenance.mjs", + "size": 2585, + "sha256": "7f698ebe33d8d3f98b31d6ae42d062e3a267c6a4907c7061e7fc638abe5fd827" + }, + { + "path": "scripts/uar-package/migration.mjs", + "size": 14885, + "sha256": "f3041068290fba7b3eafffa3fd97b67e78781b656bbef7d137376c6095300380" + }, + { + "path": "scripts/uar-package/package-files.mjs", + "size": 3656, + "sha256": "e6df13fdd969785a1c834ac74f6d031e41cab79b12fe6013ecba2649971a6acd" + }, + { + "path": "scripts/uar-package/private-authority.mjs", + "size": 3928, + "sha256": "78a28642c28f2b6b0a2b4441849cd4b181643ef9111788b7bf2f7ef23726dee6" + }, + { + "path": "scripts/uar-package/profile-validation.mjs", + "size": 7903, + "sha256": "02873529735152bc6ee344f3780fb33883d92a85d38a20cd69d5571c4524d44e" + }, + { + "path": "scripts/uar-package/workspace-questions.mjs", + "size": 5049, + "sha256": "bf504ade7776fc784637f9b0c80ec6188e1120dd4654d08a7f74ad38671e8175" + }, + { + "path": "scripts/uar-package/workspace.mjs", + "size": 25724, + "sha256": "4968bf0596ee8c84ec0626fdf18461b6e40daddc1b11af7400f6ae28ae0e257c" + }, + { + "path": "scripts/uar-package.mjs", + "size": 650, + "sha256": "4ec40be5e4ab5ae8a5b19e92ef4d753504cb98a70e0bb28cb3e6afd5dafc4b07" + }, + { + "path": "scripts/ui-bindings.mjs", + "size": 2086, + "sha256": "0e2cc2e57600bf5b3bcedbf4a7926dff85ef8cf1ded90bd7f36f75b4b2b67159" + }, + { + "path": "scripts/validation.mjs", + "size": 5822, + "sha256": "cf776cd2508a6a89702b45154258f72824eabbe1eb336b1c0bbb449d5e48f6d3" + }, + { + "path": "SKILL.md", + "size": 11431, + "sha256": "6b676fb21f543b0b32c618a70c37d91243bb40eb32b481fc16d0bb570d73edd0" + }, + { + "path": "tests/export.integration.mjs", + "size": 9132, + "sha256": "171f05587af3f955bc9ff7443c564abc8ff3f3abba688ffb2456b11034e34c1f" + }, + { + "path": "tests/fixture.mjs", + "size": 2491, + "sha256": "f5e355cba3a37c1dd4f451a3fe75306b20c7dd2dd9336b0b743f06703bc2ecc7" + }, + { + "path": "tests/kbd.integration.mjs", + "size": 9846, + "sha256": "a2cbb47c9ba58acbbc9f87b349261d59e39a2f56803122dfa292a41823ae63c4" + }, + { + "path": "tests/models-memory.integration.mjs", + "size": 16523, + "sha256": "67ad527970cd2900ff84662dce8b00bb8aafa27778247e2f07158957fdd9b15a" + }, + { + "path": "tests/state.integration.mjs", + "size": 14462, + "sha256": "cba40b6f55a8bfae3cc953b56bf441c120e12719d0be05ec232c7e9d93f7b629" + } + ] + }, + { + "name": "delivery-cadence", + "sourceDirectory": "skills/process/delivery-cadence", + "destinationDirectory": "skills/delivery-cadence", + "files": [ + { + "path": "assets/boss-provider-setup.mjs", + "size": 2511, + "sha256": "fb9d16c97ac9ea2798a110ac1d0fd7f10ee444ed6bdd5a5c5f97755d676ac9e8" + }, + { + "path": "assets/hooks/email-api.mjs", + "size": 1166, + "sha256": "c4041e45942e11b8c620477671336a134b6d692f34eca5a8e6b8fe3f3c026733" + }, + { + "path": "assets/hooks/local-summary.mjs", + "size": 851, + "sha256": "982979f0f577e73dcc795ec422d76a09283489e5685c7ef854265919a6b05ff7" + }, + { + "path": "assets/hooks/webhook.mjs", + "size": 773, + "sha256": "8a1ebebfa19995554faa858c76ddf728990792bd7fc2978477079e75c71a7607" + }, + { + "path": "references/adapters.md", + "size": 4104, + "sha256": "c2e3c086230b01126aab66d4308e34fdfda792299b812f11df6c143181f465b1" + }, + { + "path": "references/boss-profile.md", + "size": 5406, + "sha256": "f0f9341db3d27af8fb68501e7544594553372e2613f2656599366b9c92c4c7cf" + }, + { + "path": "references/child-recovery.md", + "size": 3716, + "sha256": "bb2bd77ec700e59d947021c281b679d481debd328295444865a00ff165299359" + }, + { + "path": "references/hooks.md", + "size": 4725, + "sha256": "ae599924158cad4d7d43f3121be35d2538322158f7f6e7617d8b3943e1cb6ee7" + }, + { + "path": "references/measurement.md", + "size": 4082, + "sha256": "7fe6371fa1b2ce420692f6fa269cdfa67b6ae2653c8c7d0d33d8225de1058bf8" + }, + { + "path": "references/profile.md", + "size": 6139, + "sha256": "26a78e24f73fbd88bdc5ec59de5edf2e0d2415cacecffdf68bdd6cc2556b5f9d" + }, + { + "path": "schemas/event.schema.json", + "size": 1884, + "sha256": "88330f7e1454c96a3f42b420aa0ee83badd5ca120d09327de67d73fb3cc2fb76" + }, + { + "path": "schemas/profile.schema.json", + "size": 5409, + "sha256": "f914312131d685e2c18bea1a470bcfd92cb10b3629ba712dcd925d68abcc66f6" + }, + { + "path": "schemas/state.schema.json", + "size": 24311, + "sha256": "ab8aebe94c5d7b7678c98181585a79524491f3de1a0b640e8822f2c7026f4293" + }, + { + "path": "scripts/boss-launch.mjs", + "size": 9808, + "sha256": "b65e29fed109107809706caee3dba99504a196870321d1951591978f8acb99ea" + }, + { + "path": "scripts/cadence.mjs", + "size": 1769, + "sha256": "070f3827205f7d3029a1f9184a84509ed1cb98f345fc1c580d6c20112a0bba32" + }, + { + "path": "scripts/lib/activity.mjs", + "size": 2529, + "sha256": "99987b1f09a5de0092c7de5ee56d6e015767288d931f61c88d703f7fe7f90730" + }, + { + "path": "scripts/lib/checkpoints.mjs", + "size": 12114, + "sha256": "2ca7dbad7c5a086df7cdc3b90b2996812cc74bda217a548caa63c051f96108d1" + }, + { + "path": "scripts/lib/children.mjs", + "size": 10740, + "sha256": "13fca28150ef801d343ee38cca35d35236caac133884dcdd90086ae0f6595cfb" + }, + { + "path": "scripts/lib/delivery-contract.mjs", + "size": 6352, + "sha256": "66e5f5c67f6c1809a135b33444dc7ab12e61bfd390ca701007b5633cd1321756" + }, + { + "path": "scripts/lib/engine.mjs", + "size": 23840, + "sha256": "ff77cb93bb255719dab62cd09ce5d098e1050f471a3455f92971456e7678c6df" + }, + { + "path": "scripts/lib/hook-registry.mjs", + "size": 3899, + "sha256": "6710ffd1d41af689ed1d9360ad91298e3ecf21ec1ffd1f6401613d5b77b57794" + }, + { + "path": "scripts/lib/hook-runner.mjs", + "size": 1134, + "sha256": "4c33b5495bd13f91afba7beb2344194afd69bf41b0c0fab515d694ef07975cb7" + }, + { + "path": "scripts/lib/hooks.mjs", + "size": 6256, + "sha256": "68811aa9991f2a4c7ec1a06c3119e9a88c914cffe8848490fc0e244c044f46ae" + }, + { + "path": "scripts/lib/lifecycle.mjs", + "size": 3205, + "sha256": "eead61f27daf3f491b003ba1b5f400a68020d733f428cdf3dbd9d39eb1037c5d" + }, + { + "path": "scripts/lib/process.mjs", + "size": 5097, + "sha256": "15ec8ce819c76cd0ca1034a90dfcc18ad5da411cf4f597344b9d9a655c98bbf3" + }, + { + "path": "scripts/lib/profile.mjs", + "size": 5130, + "sha256": "90844c88f39dada8c420adc6f7a9e1b9a3251af4b811d662f7e5cea8dea8330a" + }, + { + "path": "scripts/lib/report.mjs", + "size": 19317, + "sha256": "dea7343f3684e0fc93a8c96240ef915aeb5fe9ee5151cbb65bcf253482276eb8" + }, + { + "path": "scripts/lib/storage.mjs", + "size": 4042, + "sha256": "0c26074af4a1cb5d74c790b2a177cb7f9aa67b1c9e16da91c137aeb2c5ccdd29" + }, + { + "path": "scripts/publication-receipt.mjs", + "size": 3305, + "sha256": "e7bb2fac01e9fde7b9067267e185d405dbfbdb3722b0f7ff8713ede564d86ed8" + }, + { + "path": "scripts/sync-mini.mjs", + "size": 2528, + "sha256": "6d7cb7e5554bfa32e05a6b1553e18c1e7a759423f792161e14de341a30d30403" + }, + { + "path": "SKILL.md", + "size": 4740, + "sha256": "f7df45ca4988e29153e0347d79abfe2aae4b2f972e65e3aa5ceb7e7ed00e0f4b" + } + ] + } + ] +} diff --git a/docs/agent-team-creator-c094.md b/docs/agent-team-creator-c094.md new file mode 100644 index 0000000..eff79ca --- /dev/null +++ b/docs/agent-team-creator-c094.md @@ -0,0 +1,9 @@ +# C09.4 shared-skill distribution + +The mini pack carries the complete Agent Team Creator 1.3.0 and Delivery Cadence 1.1.2 payloads copied from full-pack commit `1ddcc8b21b05f26aa89aa5e19776c86e5b854c96`. The [distribution receipt](agent-team-creator-c094-distribution.json) records each copied file's size and SHA-256 digest. Runtime dependencies are included; `node_modules` and Git metadata are excluded. + +Creator includes the draft.2 authoring workspace, immutable package and binding maintenance, shared team instructions, directed cooperation guidance, and exact schemas derived from UAR commit `0db89a46db5bd590e5d9a9732389f41e3785bdb1`. Its schema source receipt remains identical to the full pack. Cadence includes the approved process-timeout correction requiring an explicit reason and retaining configured/requested bounds without changing the frozen command, scope, or hard budgets. + +Existing skill discovery scans `skills/*/SKILL.md`; installer and application packaging recursively include the complete skill directory. No new discovery entry, shell script, daemon, or host dependency installation was added. Node.js 22+ runs the emitted `.mjs` payload. Other skills and the liter-llm gitlink remain untouched. + +This receipt proves source distribution provenance, not installed execution, architecture qualification, or platform certification. The completed delivery's real build and operation boundary supplies those results separately. No test suite, compiler check, or application build was run for this copy. diff --git a/skills/agent-team-creator/SKILL.md b/skills/agent-team-creator/SKILL.md index 19e3ef0..e51b67a 100644 --- a/skills/agent-team-creator/SKILL.md +++ b/skills/agent-team-creator/SKILL.md @@ -4,7 +4,7 @@ description: "Create, revise, package, and deploy agent-team definitions with st license: MIT compatibility: Requires Node.js 22 or newer. Git is optional for handoff snapshots. Model gateways, memory services and native harness CLIs are optional and separately configured. metadata: - version: "1.2.0" + version: "1.3.0" tags: "agents, teams, orchestration, coding" --- @@ -18,9 +18,12 @@ concept or start a second agent loop. ## Start with the task Read project instructions and any active KBD work first. Reuse answers already -given. Start by distinguishing **create**, **revise**, and **deploy**. Creation +given. Start by distinguishing local **create**, persisted UAR **author**, +immutable **revise**, and **deploy**. Creation asks only for missing outcome, scope, deliverables, budget preference and review -needs. Revision asks for the current definition/package, desired change, next +needs. UAR authoring reads the selected `.agent-team//authoring` +workspace and returns one persisted missing document field or graph relationship +at a time. Revision asks for the current workspace revision, desired change, next semantic version and deployment intent. Deployment asks for the reviewed package, UAR instance, environment credential reference and whether a private binding is also required. The user need not know agent terminology. @@ -126,20 +129,33 @@ native reference for supported project agents or plugin/marketplace installation Do not invent plugin agent fields where a harness has none. A plugin installation does not start an agent team. -For UAR, use the canonical package and private binding path in +For UAR, first clarify shared behavioral guidance (or explicitly none), exact member skills/tools, required versus optional context resources, and manual versus cooperating-pair execution. Reuse known answers. Directed delegation requires a trigger-turn edge, and worker results need a separately permitted return edge. Catalog validity alone does not certify a runnable execution profile. + +Use the draft.2 file-backed workspace, canonical package, and private binding path in [UAR deployment](references/uar-deployment.md). It preserves complete AgentDefinition, TeamDefinition, WorkflowDefinition, PackageManifest and DeploymentBinding fields, resolves immutable references, and installs the whole -package atomically through the collaboration catalog API. Use an operator-selected +package atomically through the collaboration catalog API. `workspace.json` names +one manifest source and separate agent, team, subteam, and workflow documents. +Use `uar-workspace-status` or `guide` with `operation=author`; each response gives +the monotonic revision, fixed counts, one stable next-question ID, saved-answer +progress, and a bounded diagnostic page without returning the graph. Apply an +answer with `uar-workspace-answer`, or update one declared file with +`uar-workspace-update`; every mutation requires the last observed +`expectedRevision` and refuses stale state before writing. Use an operator-selected instance URL and `env:VARIABLE` credential reference. Run capability discovery, package preflight, install and exact status before an optional binding preflight -and install. Catalog installation is not activation: the `uar-activate` command -refuses until the durable I2 team runtime exists. +and install. Package installation confers no credential, representation grant, +consent, authority, or activation. The `uar-activate` command refuses because +this authoring client does not own execution. Operate through The Boss or another +authorized host after negotiating the selected UAR instance's execution profile. Schema-v1 `export --target uar` remains a compatibility projection for existing single-agent consumers. It cannot represent a UAR team and must not be used to claim collaboration-package deployment. Create new UAR teams through -`uar-package-build` and `uar-package-install`. +`uar-workspace-init`, document updates, `uar-package-build`, and +`uar-package-install`. Existing draft.1 inline packages remain readable and +migrate explicitly; they are never rewritten or relabeled. For BossFang, keep registry registration, activation and execution separate. BossFang Hands and standalone agent/workflow registration are alternative native diff --git a/skills/agent-team-creator/assets/deploy-intake.json b/skills/agent-team-creator/assets/deploy-intake.json index 87f9407..ce6aa9b 100644 --- a/skills/agent-team-creator/assets/deploy-intake.json +++ b/skills/agent-team-creator/assets/deploy-intake.json @@ -3,6 +3,6 @@ "baseUrl": "http://127.0.0.1:1906", "credentialRef": "env:UAR_TOKEN", "packageDirectory": ".agent-team/checkout-team/uar-package-1.1.0", - "bindingIntent": "package-and-binding" + "bindingIntent": "package-and-binding", + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2" } - diff --git a/skills/agent-team-creator/assets/revise-intake.json b/skills/agent-team-creator/assets/revise-intake.json index a981f17..65720b4 100644 --- a/skills/agent-team-creator/assets/revise-intake.json +++ b/skills/agent-team-creator/assets/revise-intake.json @@ -1,8 +1,8 @@ { - "operation": "revise", - "state": ".agent-team/checkout-team/state.json", - "changeSummary": "Add a release coordinator and bind a new workflow revision", + "project": ".", + "workspace": "checkout-team", + "expectedRevision": 1, + "out": "checkout-team-v2", "nextVersion": "1.1.0", - "deploymentIntent": "stage" + "edits": [] } - diff --git a/skills/agent-team-creator/assets/uar-intake.json b/skills/agent-team-creator/assets/uar-intake.json index 7baf355..9a4740d 100644 --- a/skills/agent-team-creator/assets/uar-intake.json +++ b/skills/agent-team-creator/assets/uar-intake.json @@ -1,37 +1,17 @@ { - "operation": "create", - "id": "checkout-team", - "outcome": "Implement an accessible checkout and produce one complete integration result", - "complexity": "complex", - "areas": ["code", "design", "security"], - "deliverables": ["Implementation", "Design artifact", "Integration evidence"], - "budget": "balanced", - "review": true, - "harness": "uar", - "scope": "uar", - "ownership": { - "implementer": ["src/checkout/**"], - "designer": ["docs/design/checkout/**"], - "security-reviewer": ["docs/security/checkout-review.md"], - "reviewer": ["docs/reviews/checkout-review.md"] - }, + "project": ".", + "workspace": "checkout-team", + "expectedRevision": 0, "packageId": "urn:example:checkout-team", "packageVersion": "1.0.0", - "coordinatorRole": "implementer", - "workflowSummary": "Design and implementation finish before one integration task; external writes require current authority.", - "communicationPolicy": "Coordinator may queue or trigger members; members may queue replies to coordinator.", - "aggregateLimits": { - "concurrentTurns": 4, - "maxMembers": 8, - "maxDepth": 2, - "maxPendingTasks": 100 - }, - "aggregateBudget": { - "maxTokens": 200000, - "maxCostMicrounits": 10000000, - "currency": "USD", - "maxElapsedSeconds": 7200 - }, + "manifestPath": "manifest.source.json", + "definitionPaths": [ + "agents/coordinator.json", + "agents/implementer.json", + "agents/reviewer.json", + "teams/root.json", + "teams/review-subteam.json", + "workflows/checkout.json" + ], "bindingIntent": "package-and-binding" } - diff --git a/skills/agent-team-creator/assets/uar-workspace/agents/child.json b/skills/agent-team-creator/assets/uar-workspace/agents/child.json new file mode 100644 index 0000000..f0fea57 --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/agents/child.json @@ -0,0 +1,174 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "AgentDefinition", + "id": "urn:prometheus:mini:example:agent:child", + "version": "1.0.0", + "provenance": { + "source": "UAR collaboration draft.2 contract checkpoint fixtures", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [ + "collaboration_definition_packages_v2" + ], + "extensions": { + "example.optional": { + "required": false, + "value": { + "retained": true + } + } + }, + "title": "Permitted child", + "role": "child", + "whenToUse": "Use for the child responsibility in the example team.", + "instructions": "Complete the bounded child responsibility and return the declared output.", + "input": { + "type": "object", + "properties": { + "brief": { + "type": "string" + } + }, + "required": [ + "brief" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "skills": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ], + "models": [ + { + "role": "primary", + "capabilities": [ + "text", + "tool-calling" + ], + "preferredAliases": [ + "team-default" + ] + } + ], + "permittedChildren": [], + "context": { + "mode": "selected", + "artifacts": [ + "task-input" + ], + "history": "authorized-summary", + "memoryScopes": [] + }, + "requestedLimits": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + }, + "sourceIdentity": { + "profile": "urn:prometheus:uar:agent-md:1.1", + "id": "urn:prometheus:mini:legacy:child", + "version": "1.0.0", + "digest": "sha256:ae80029bfd3fff39253a18d76f5e628fcf6e8fcea8e22013a59bc2ef388f5ae2", + "revision": null + }, + "renameMapping": { + "sourceId": "urn:prometheus:mini:legacy:child", + "targetId": "urn:prometheus:mini:example:agent:child", + "reason": "explicit-rename" + }, + "authoredFields": [ + "/metadata/id", + "/metadata/version", + "/skills/0", + "/model_requirements", + "/prompt_dialect", + "/rag_configuration", + "/context_strategy", + "/api_harness" + ], + "modelRequirements": { + "required": true, + "value": { + "capabilities": [ + "text", + "tool-calling" + ] + } + }, + "promptDialect": { + "required": true, + "value": { + "id": "uar.prompt/plain-v1" + } + }, + "ragConfiguration": { + "required": false, + "value": { + "mode": "disabled" + } + }, + "contextStrategy": { + "required": true, + "value": { + "mode": "selected" + } + }, + "apiHarness": { + "required": false, + "value": { + "id": "uar.ordinary-agent/1" + } + }, + "legacySections": { + "skills": "review@2.1.0" + }, + "sourceDescriptor": { + "metadata": { + "id": "urn:uar:legacy:reviewer", + "version": "1.0.0" + }, + "skills": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ] + } +} diff --git a/skills/agent-team-creator/assets/uar-workspace/agents/coordinator.json b/skills/agent-team-creator/assets/uar-workspace/agents/coordinator.json new file mode 100644 index 0000000..614f0b6 --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/agents/coordinator.json @@ -0,0 +1,179 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "AgentDefinition", + "id": "urn:prometheus:mini:example:agent:coordinator", + "version": "1.0.0", + "provenance": { + "source": "UAR collaboration draft.2 contract checkpoint fixtures", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [ + "collaboration_definition_packages_v2" + ], + "extensions": { + "example.optional": { + "required": false, + "value": { + "retained": true + } + } + }, + "title": "Coordinator", + "role": "coordinator", + "whenToUse": "Use for the coordinator responsibility in the example team.", + "instructions": "Complete the bounded coordinator responsibility and return the declared output.", + "input": { + "type": "object", + "properties": { + "brief": { + "type": "string" + } + }, + "required": [ + "brief" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "skills": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ], + "models": [ + { + "role": "primary", + "capabilities": [ + "text", + "tool-calling" + ], + "preferredAliases": [ + "team-default" + ] + } + ], + "permittedChildren": [ + { + "id": "urn:prometheus:mini:example:agent:child", + "version": "1.0.0" + } + ], + "context": { + "mode": "selected", + "artifacts": [ + "task-input" + ], + "history": "authorized-summary", + "memoryScopes": [] + }, + "requestedLimits": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + }, + "sourceIdentity": { + "profile": "urn:prometheus:uar:agent-md:1.1", + "id": "urn:prometheus:mini:legacy:coordinator", + "version": "1.0.0", + "digest": "sha256:ae80029bfd3fff39253a18d76f5e628fcf6e8fcea8e22013a59bc2ef388f5ae2", + "revision": null + }, + "renameMapping": { + "sourceId": "urn:prometheus:mini:legacy:coordinator", + "targetId": "urn:prometheus:mini:example:agent:coordinator", + "reason": "explicit-rename" + }, + "authoredFields": [ + "/metadata/id", + "/metadata/version", + "/skills/0", + "/model_requirements", + "/prompt_dialect", + "/rag_configuration", + "/context_strategy", + "/api_harness" + ], + "modelRequirements": { + "required": true, + "value": { + "capabilities": [ + "text", + "tool-calling" + ] + } + }, + "promptDialect": { + "required": true, + "value": { + "id": "uar.prompt/plain-v1" + } + }, + "ragConfiguration": { + "required": false, + "value": { + "mode": "disabled" + } + }, + "contextStrategy": { + "required": true, + "value": { + "mode": "selected" + } + }, + "apiHarness": { + "required": false, + "value": { + "id": "uar.ordinary-agent/1" + } + }, + "legacySections": { + "skills": "review@2.1.0" + }, + "sourceDescriptor": { + "metadata": { + "id": "urn:uar:legacy:reviewer", + "version": "1.0.0" + }, + "skills": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ] + } +} diff --git a/skills/agent-team-creator/assets/uar-workspace/agents/specialist.json b/skills/agent-team-creator/assets/uar-workspace/agents/specialist.json new file mode 100644 index 0000000..8e638cf --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/agents/specialist.json @@ -0,0 +1,174 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "AgentDefinition", + "id": "urn:prometheus:mini:example:agent:specialist", + "version": "1.0.0", + "provenance": { + "source": "UAR collaboration draft.2 contract checkpoint fixtures", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [ + "collaboration_definition_packages_v2" + ], + "extensions": { + "example.optional": { + "required": false, + "value": { + "retained": true + } + } + }, + "title": "Specialist", + "role": "specialist", + "whenToUse": "Use for the specialist responsibility in the example team.", + "instructions": "Complete the bounded specialist responsibility and return the declared output.", + "input": { + "type": "object", + "properties": { + "brief": { + "type": "string" + } + }, + "required": [ + "brief" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "skills": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ], + "models": [ + { + "role": "primary", + "capabilities": [ + "text", + "tool-calling" + ], + "preferredAliases": [ + "team-default" + ] + } + ], + "permittedChildren": [], + "context": { + "mode": "selected", + "artifacts": [ + "task-input" + ], + "history": "authorized-summary", + "memoryScopes": [] + }, + "requestedLimits": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + }, + "sourceIdentity": { + "profile": "urn:prometheus:uar:agent-md:1.1", + "id": "urn:prometheus:mini:legacy:specialist", + "version": "1.0.0", + "digest": "sha256:ae80029bfd3fff39253a18d76f5e628fcf6e8fcea8e22013a59bc2ef388f5ae2", + "revision": null + }, + "renameMapping": { + "sourceId": "urn:prometheus:mini:legacy:specialist", + "targetId": "urn:prometheus:mini:example:agent:specialist", + "reason": "explicit-rename" + }, + "authoredFields": [ + "/metadata/id", + "/metadata/version", + "/skills/0", + "/model_requirements", + "/prompt_dialect", + "/rag_configuration", + "/context_strategy", + "/api_harness" + ], + "modelRequirements": { + "required": true, + "value": { + "capabilities": [ + "text", + "tool-calling" + ] + } + }, + "promptDialect": { + "required": true, + "value": { + "id": "uar.prompt/plain-v1" + } + }, + "ragConfiguration": { + "required": false, + "value": { + "mode": "disabled" + } + }, + "contextStrategy": { + "required": true, + "value": { + "mode": "selected" + } + }, + "apiHarness": { + "required": false, + "value": { + "id": "uar.ordinary-agent/1" + } + }, + "legacySections": { + "skills": "review@2.1.0" + }, + "sourceDescriptor": { + "metadata": { + "id": "urn:uar:legacy:reviewer", + "version": "1.0.0" + }, + "skills": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ] + } +} diff --git a/skills/agent-team-creator/assets/uar-workspace/manifest.source.json b/skills/agent-team-creator/assets/uar-workspace/manifest.source.json new file mode 100644 index 0000000..3dcbd95 --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/manifest.source.json @@ -0,0 +1,29 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "PackageManifest", + "id": "urn:prometheus:mini:example:package", + "version": "1.0.0", + "provenance": { + "source": "agent-team-creator split workspace example", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [ + "collaboration_definition_packages_v2" + ], + "extensions": {}, + "entrypoints": [ + { + "id": "urn:prometheus:mini:example:team:root", + "version": "1.0.0" + } + ], + "capabilityDeclarations": [ + { + "capability": "collaboration_definition_packages_v2", + "required": true + } + ], + "resolution": "exact-version-and-digest" +} diff --git a/skills/agent-team-creator/assets/uar-workspace/teams/root.json b/skills/agent-team-creator/assets/uar-workspace/teams/root.json new file mode 100644 index 0000000..6401784 --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/teams/root.json @@ -0,0 +1,108 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "TeamDefinition", + "id": "urn:prometheus:mini:example:team:root", + "version": "1.0.0", + "provenance": { + "source": "UAR collaboration draft.2 contract checkpoint fixtures", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [], + "extensions": {}, + "title": "Root collaboration team", + "purpose": "Demonstrate multiple agents, a permitted child, a nested subteam, and a workflow.", + "members": [ + { + "role": "coordinator", + "kind": "agent", + "definition": { + "id": "urn:prometheus:mini:example:agent:coordinator", + "version": "1.0.0" + }, + "min": 1, + "max": 1, + "responsibility": "Coordinate the bounded workflow." + }, + { + "role": "research", + "kind": "team", + "definition": { + "id": "urn:prometheus:mini:example:team:subteam", + "version": "1.0.0" + }, + "min": 1, + "max": 1, + "responsibility": "Produce the nested specialist review." + } + ], + "coordinatorRole": "coordinator", + "communication": [ + { + "fromRole": "coordinator", + "toRole": "research", + "modes": [ + "queue-only", + "trigger-turn" + ] + }, + { + "fromRole": "research", + "toRole": "coordinator", + "modes": [ + "queue-only" + ] + } + ], + "taskAcceptance": { + "mode": "operator", + "allowedWorkflows": [ + { + "id": "urn:prometheus:mini:example:workflow:review", + "version": "1.0.0" + } + ] + }, + "routing": { + "eligibilityFirst": true, + "strategy": "operator-role-capacity-cost-stable-id", + "explain": true + }, + "limits": { + "concurrentTurns": 2, + "maxMembers": 4, + "maxDepth": 2, + "maxPendingTasks": 16 + }, + "budget": { + "maxTokens": 10000, + "maxCostMicrounits": 1000000, + "currency": "USD", + "maxElapsedSeconds": 300 + }, + "input": { + "type": "object", + "properties": { + "brief": { + "type": "string" + } + }, + "required": [ + "brief" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + } +} diff --git a/skills/agent-team-creator/assets/uar-workspace/teams/subteam.json b/skills/agent-team-creator/assets/uar-workspace/teams/subteam.json new file mode 100644 index 0000000..d4e8fa0 --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/teams/subteam.json @@ -0,0 +1,81 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "TeamDefinition", + "id": "urn:prometheus:mini:example:team:subteam", + "version": "1.0.0", + "provenance": { + "source": "UAR collaboration draft.2 contract checkpoint fixtures", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [], + "extensions": {}, + "title": "Nested specialist team", + "purpose": "Demonstrate a kind-correct nested team.", + "members": [ + { + "role": "specialist", + "kind": "agent", + "definition": { + "id": "urn:prometheus:mini:example:agent:specialist", + "version": "1.0.0" + }, + "min": 1, + "max": 1, + "responsibility": "Produce a bounded specialist artifact." + } + ], + "coordinatorRole": "specialist", + "communication": [], + "taskAcceptance": { + "mode": "operator", + "allowedWorkflows": [ + { + "id": "urn:prometheus:mini:example:workflow:specialist", + "version": "1.0.0" + } + ] + }, + "routing": { + "eligibilityFirst": true, + "strategy": "operator-role-capacity-cost-stable-id", + "explain": true + }, + "limits": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + }, + "budget": { + "maxTokens": 10000, + "maxCostMicrounits": 1000000, + "currency": "USD", + "maxElapsedSeconds": 300 + }, + "input": { + "type": "object", + "properties": { + "brief": { + "type": "string" + } + }, + "required": [ + "brief" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + } +} diff --git a/skills/agent-team-creator/assets/uar-workspace/workflows/review.json b/skills/agent-team-creator/assets/uar-workspace/workflows/review.json new file mode 100644 index 0000000..8b2352d --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/workflows/review.json @@ -0,0 +1,101 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "WorkflowDefinition", + "id": "urn:prometheus:mini:example:workflow:review", + "version": "1.0.0", + "provenance": { + "source": "UAR collaboration draft.2 contract checkpoint fixtures", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [], + "extensions": {}, + "title": "Coordinate then review nested work", + "input": { + "type": "object", + "properties": { + "brief": { + "type": "string" + } + }, + "required": [ + "brief" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "steps": [ + { + "id": "coordinate", + "role": "coordinator", + "dependsOn": [], + "inputMapping": { + "brief": "workflow-input:brief" + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "effect": "read", + "approval": "current-authority", + "retry": { + "maxAttempts": 1, + "onUnknownEffect": "reconcile-before-retry" + }, + "completion": "artifact", + "instructions": "Prepare the bounded review brief." + }, + { + "id": "review", + "role": "research", + "dependsOn": [ + "coordinate" + ], + "inputMapping": { + "brief": "workflow-input:brief" + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "effect": "read", + "approval": "current-authority", + "retry": { + "maxAttempts": 1, + "onUnknownEffect": "reconcile-before-retry" + }, + "completion": "artifact", + "instructions": "Review the brief through the nested team." + } + ], + "failurePolicy": "stop-dependent", + "maxActivations": 1 +} diff --git a/skills/agent-team-creator/assets/uar-workspace/workflows/specialist.json b/skills/agent-team-creator/assets/uar-workspace/workflows/specialist.json new file mode 100644 index 0000000..48fee85 --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/workflows/specialist.json @@ -0,0 +1,71 @@ +{ + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "kind": "WorkflowDefinition", + "id": "urn:prometheus:mini:example:workflow:specialist", + "version": "1.0.0", + "provenance": { + "source": "UAR collaboration draft.2 contract checkpoint fixtures", + "authors": [ + "Prometheus-AGS" + ] + }, + "requiredCapabilities": [], + "extensions": {}, + "title": "Produce a bounded specialist artifact", + "input": { + "type": "object", + "properties": { + "brief": { + "type": "string" + } + }, + "required": [ + "brief" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "steps": [ + { + "id": "specialist-artifact", + "role": "specialist", + "dependsOn": [], + "inputMapping": { + "brief": "workflow-input:brief" + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "effect": "read", + "approval": "current-authority", + "retry": { + "maxAttempts": 1, + "onUnknownEffect": "reconcile-before-retry" + }, + "completion": "artifact", + "instructions": "Produce one bounded specialist artifact from the brief." + } + ], + "failurePolicy": "stop-dependent", + "maxActivations": 1 +} diff --git a/skills/agent-team-creator/assets/uar-workspace/workspace.json b/skills/agent-team-creator/assets/uar-workspace/workspace.json new file mode 100644 index 0000000..0576b1b --- /dev/null +++ b/skills/agent-team-creator/assets/uar-workspace/workspace.json @@ -0,0 +1,610 @@ +{ + "schemaVersion": 1, + "revision": 1, + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "teamId": "example-team", + "packageId": "urn:prometheus:mini:example:package", + "packageVersion": "1.0.0", + "manifest": "manifest.source.json", + "definitions": [ + "agents/coordinator.json", + "agents/specialist.json", + "agents/child.json", + "teams/root.json", + "teams/subteam.json", + "workflows/review.json", + "workflows/specialist.json" + ], + "bindingIntent": "package-only", + "questionState": { + "currentQuestionId": null, + "questions": [ + { + "id": "manifest.provenance", + "document": "manifest.source.json", + "pointer": "/provenance", + "prompt": "What source and authors establish package provenance?", + "required": true, + "state": "answered", + "answer": { + "source": "agent-team-creator split workspace example", + "authors": [ + "Prometheus-AGS" + ] + } + }, + { + "id": "manifest.entrypoint", + "document": "manifest.source.json", + "pointer": "/entrypoints", + "prompt": "Which single TeamDefinition is the top-level package entrypoint?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "urn:prometheus:mini:example:team:root", + "version": "1.0.0" + } + ] + }, + { + "id": "team.urn:prometheus:mini:example:team:root.members", + "document": "teams/root.json", + "pointer": "/members", + "prompt": "Which agents or nested teams belong to urn:prometheus:mini:example:team:root?", + "required": true, + "state": "answered", + "answer": [ + { + "role": "coordinator", + "kind": "agent", + "definition": { + "id": "urn:prometheus:mini:example:agent:coordinator", + "version": "1.0.0" + }, + "min": 1, + "max": 1, + "responsibility": "Coordinate the bounded workflow." + }, + { + "role": "research", + "kind": "team", + "definition": { + "id": "urn:prometheus:mini:example:team:subteam", + "version": "1.0.0" + }, + "min": 1, + "max": 1, + "responsibility": "Produce the nested specialist review." + } + ] + }, + { + "id": "team.urn:prometheus:mini:example:team:root.coordinator", + "document": "teams/root.json", + "pointer": "/coordinatorRole", + "prompt": "Which agent role coordinates urn:prometheus:mini:example:team:root?", + "required": true, + "state": "answered", + "answer": "coordinator" + }, + { + "id": "team.urn:prometheus:mini:example:team:root.communication", + "document": "teams/root.json", + "pointer": "/communication", + "prompt": "Which explicit role communication edges are allowed for urn:prometheus:mini:example:team:root?", + "required": true, + "state": "answered", + "answer": [ + { + "fromRole": "coordinator", + "toRole": "research", + "modes": [ + "queue-only", + "trigger-turn" + ] + }, + { + "fromRole": "research", + "toRole": "coordinator", + "modes": [ + "queue-only" + ] + } + ] + }, + { + "id": "team.urn:prometheus:mini:example:team:root.acceptance", + "document": "teams/root.json", + "pointer": "/taskAcceptance/allowedWorkflows", + "prompt": "Which exact workflows may urn:prometheus:mini:example:team:root accept?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "urn:prometheus:mini:example:workflow:review", + "version": "1.0.0" + } + ] + }, + { + "id": "team.urn:prometheus:mini:example:team:root.limits", + "document": "teams/root.json", + "pointer": "/limits", + "prompt": "Which aggregate limits constrain urn:prometheus:mini:example:team:root?", + "required": true, + "state": "answered", + "answer": { + "concurrentTurns": 2, + "maxMembers": 4, + "maxDepth": 2, + "maxPendingTasks": 16 + } + }, + { + "id": "team.urn:prometheus:mini:example:team:root.budget", + "document": "teams/root.json", + "pointer": "/budget", + "prompt": "Which aggregate budget constrains urn:prometheus:mini:example:team:root?", + "required": true, + "state": "answered", + "answer": { + "maxTokens": 10000, + "maxCostMicrounits": 1000000, + "currency": "USD", + "maxElapsedSeconds": 300 + } + }, + { + "id": "team.urn:prometheus:mini:example:team:subteam.members", + "document": "teams/subteam.json", + "pointer": "/members", + "prompt": "Which agents or nested teams belong to urn:prometheus:mini:example:team:subteam?", + "required": true, + "state": "answered", + "answer": [ + { + "role": "specialist", + "kind": "agent", + "definition": { + "id": "urn:prometheus:mini:example:agent:specialist", + "version": "1.0.0" + }, + "min": 1, + "max": 1, + "responsibility": "Produce a bounded specialist artifact." + } + ] + }, + { + "id": "team.urn:prometheus:mini:example:team:subteam.coordinator", + "document": "teams/subteam.json", + "pointer": "/coordinatorRole", + "prompt": "Which agent role coordinates urn:prometheus:mini:example:team:subteam?", + "required": true, + "state": "answered", + "answer": "specialist" + }, + { + "id": "team.urn:prometheus:mini:example:team:subteam.communication", + "document": "teams/subteam.json", + "pointer": "/communication", + "prompt": "Which explicit role communication edges are allowed for urn:prometheus:mini:example:team:subteam?", + "required": true, + "state": "answered", + "answer": [] + }, + { + "id": "team.urn:prometheus:mini:example:team:subteam.acceptance", + "document": "teams/subteam.json", + "pointer": "/taskAcceptance/allowedWorkflows", + "prompt": "Which exact workflows may urn:prometheus:mini:example:team:subteam accept?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "urn:prometheus:mini:example:workflow:specialist", + "version": "1.0.0" + } + ] + }, + { + "id": "team.urn:prometheus:mini:example:team:subteam.limits", + "document": "teams/subteam.json", + "pointer": "/limits", + "prompt": "Which aggregate limits constrain urn:prometheus:mini:example:team:subteam?", + "required": true, + "state": "answered", + "answer": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + } + }, + { + "id": "team.urn:prometheus:mini:example:team:subteam.budget", + "document": "teams/subteam.json", + "pointer": "/budget", + "prompt": "Which aggregate budget constrains urn:prometheus:mini:example:team:subteam?", + "required": true, + "state": "answered", + "answer": { + "maxTokens": 10000, + "maxCostMicrounits": 1000000, + "currency": "USD", + "maxElapsedSeconds": 300 + } + }, + { + "id": "agent.urn:prometheus:mini:example:agent:coordinator.children", + "document": "agents/coordinator.json", + "pointer": "/permittedChildren", + "prompt": "Which exact child agents may urn:prometheus:mini:example:agent:coordinator invoke?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "urn:prometheus:mini:example:agent:child", + "version": "1.0.0" + } + ] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:coordinator.skills", + "document": "agents/coordinator.json", + "pointer": "/skills", + "prompt": "Which exact skill locks does urn:prometheus:mini:example:agent:coordinator require?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:coordinator.models", + "document": "agents/coordinator.json", + "pointer": "/models", + "prompt": "Which model capabilities and aliases does urn:prometheus:mini:example:agent:coordinator request?", + "required": true, + "state": "answered", + "answer": [ + { + "role": "primary", + "capabilities": [ + "text", + "tool-calling" + ], + "preferredAliases": [ + "team-default" + ] + } + ] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:coordinator.context", + "document": "agents/coordinator.json", + "pointer": "/context", + "prompt": "Which bounded context may urn:prometheus:mini:example:agent:coordinator receive?", + "required": true, + "state": "answered", + "answer": { + "mode": "selected", + "artifacts": [ + "task-input" + ], + "history": "authorized-summary", + "memoryScopes": [] + } + }, + { + "id": "agent.urn:prometheus:mini:example:agent:coordinator.limits", + "document": "agents/coordinator.json", + "pointer": "/requestedLimits", + "prompt": "Which limits constrain urn:prometheus:mini:example:agent:coordinator?", + "required": true, + "state": "answered", + "answer": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + } + }, + { + "id": "agent.urn:prometheus:mini:example:agent:specialist.children", + "document": "agents/specialist.json", + "pointer": "/permittedChildren", + "prompt": "Which exact child agents may urn:prometheus:mini:example:agent:specialist invoke?", + "required": true, + "state": "answered", + "answer": [] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:specialist.skills", + "document": "agents/specialist.json", + "pointer": "/skills", + "prompt": "Which exact skill locks does urn:prometheus:mini:example:agent:specialist require?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:specialist.models", + "document": "agents/specialist.json", + "pointer": "/models", + "prompt": "Which model capabilities and aliases does urn:prometheus:mini:example:agent:specialist request?", + "required": true, + "state": "answered", + "answer": [ + { + "role": "primary", + "capabilities": [ + "text", + "tool-calling" + ], + "preferredAliases": [ + "team-default" + ] + } + ] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:specialist.context", + "document": "agents/specialist.json", + "pointer": "/context", + "prompt": "Which bounded context may urn:prometheus:mini:example:agent:specialist receive?", + "required": true, + "state": "answered", + "answer": { + "mode": "selected", + "artifacts": [ + "task-input" + ], + "history": "authorized-summary", + "memoryScopes": [] + } + }, + { + "id": "agent.urn:prometheus:mini:example:agent:specialist.limits", + "document": "agents/specialist.json", + "pointer": "/requestedLimits", + "prompt": "Which limits constrain urn:prometheus:mini:example:agent:specialist?", + "required": true, + "state": "answered", + "answer": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + } + }, + { + "id": "agent.urn:prometheus:mini:example:agent:child.children", + "document": "agents/child.json", + "pointer": "/permittedChildren", + "prompt": "Which exact child agents may urn:prometheus:mini:example:agent:child invoke?", + "required": true, + "state": "answered", + "answer": [] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:child.skills", + "document": "agents/child.json", + "pointer": "/skills", + "prompt": "Which exact skill locks does urn:prometheus:mini:example:agent:child require?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "urn:uar:skill:review", + "version": "2.1.0", + "digest": "sha256:3b3816290748ea6b1f71c43c34e4043e2aed44ff4b7a462e7b212bf88cf0b65f", + "required": true, + "config": { + "mode": "strict", + "output": "json" + }, + "entrypoint": "review", + "requiredTools": [ + "read-artifact" + ] + } + ] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:child.models", + "document": "agents/child.json", + "pointer": "/models", + "prompt": "Which model capabilities and aliases does urn:prometheus:mini:example:agent:child request?", + "required": true, + "state": "answered", + "answer": [ + { + "role": "primary", + "capabilities": [ + "text", + "tool-calling" + ], + "preferredAliases": [ + "team-default" + ] + } + ] + }, + { + "id": "agent.urn:prometheus:mini:example:agent:child.context", + "document": "agents/child.json", + "pointer": "/context", + "prompt": "Which bounded context may urn:prometheus:mini:example:agent:child receive?", + "required": true, + "state": "answered", + "answer": { + "mode": "selected", + "artifacts": [ + "task-input" + ], + "history": "authorized-summary", + "memoryScopes": [] + } + }, + { + "id": "agent.urn:prometheus:mini:example:agent:child.limits", + "document": "agents/child.json", + "pointer": "/requestedLimits", + "prompt": "Which limits constrain urn:prometheus:mini:example:agent:child?", + "required": true, + "state": "answered", + "answer": { + "concurrentTurns": 1, + "maxMembers": 2, + "maxDepth": 1, + "maxPendingTasks": 8 + } + }, + { + "id": "workflow.urn:prometheus:mini:example:workflow:review.steps", + "document": "workflows/review.json", + "pointer": "/steps", + "prompt": "Which finite role steps define urn:prometheus:mini:example:workflow:review?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "coordinate", + "role": "coordinator", + "dependsOn": [], + "inputMapping": { + "brief": "workflow-input:brief" + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "effect": "read", + "approval": "current-authority", + "retry": { + "maxAttempts": 1, + "onUnknownEffect": "reconcile-before-retry" + }, + "completion": "artifact", + "instructions": "Prepare the bounded review brief." + }, + { + "id": "review", + "role": "research", + "dependsOn": [ + "coordinate" + ], + "inputMapping": { + "brief": "workflow-input:brief" + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "effect": "read", + "approval": "current-authority", + "retry": { + "maxAttempts": 1, + "onUnknownEffect": "reconcile-before-retry" + }, + "completion": "artifact", + "instructions": "Review the brief through the nested team." + } + ] + }, + { + "id": "workflow.urn:prometheus:mini:example:workflow:specialist.steps", + "document": "workflows/specialist.json", + "pointer": "/steps", + "prompt": "Which finite role steps define urn:prometheus:mini:example:workflow:specialist?", + "required": true, + "state": "answered", + "answer": [ + { + "id": "specialist-artifact", + "role": "specialist", + "dependsOn": [], + "inputMapping": { + "brief": "workflow-input:brief" + }, + "output": { + "type": "object", + "properties": { + "artifact": { + "type": "string" + } + }, + "required": [ + "artifact" + ], + "additionalProperties": false + }, + "effect": "read", + "approval": "current-authority", + "retry": { + "maxAttempts": 1, + "onUnknownEffect": "reconcile-before-retry" + }, + "completion": "artifact", + "instructions": "Produce one bounded specialist artifact from the brief." + } + ] + }, + { + "id": "workspace.binding-intent", + "document": "workspace.json", + "pointer": "/bindingIntent", + "prompt": "Should deployment stop after package installation or prepare a private binding?", + "required": true, + "state": "answered", + "answer": "package-only" + } + ] + } +} diff --git a/skills/agent-team-creator/references/manifest.md b/skills/agent-team-creator/references/manifest.md index 088484f..471cc19 100644 --- a/skills/agent-team-creator/references/manifest.md +++ b/skills/agent-team-creator/references/manifest.md @@ -114,7 +114,7 @@ node /scripts/cli.mjs --input request.json | Command | Request | |---|---| -| `guide` | `operation=create|revise|deploy` plus the staged intake fields returned for that operation. Create remains the default for schema-v1 callers. | +| `guide` | `operation=create|author|revise|deploy`; `author` reads `project` plus `workspace` and returns one next question with bounded status. Create remains the default for schema-v1 callers. | | `validate` | `team` manifest | | `init` | `team`, new `state` filename | | `status` | `state` | @@ -124,22 +124,30 @@ node /scripts/cli.mjs --input request.json | `handoff-create`, `handoff-accept` | See `task-handoff.md` | | `models-discover`, `models-select` | See `models-memory.md` | | `memory-queue`, `memory-publish` | See `models-memory.md` | -| `uar-package-validate` | `package` authoring envelope from `schemas/uar-package-authoring.schema.json` | -| `uar-package-build` | `package`, new `out` directory | -| `uar-package-diff` | `before` and `after` authoring packages with the same package ID | +| `uar-workspace-init` | `project`, portable team ID in `workspace`, `expectedRevision: 0`, package identity/version, and declared source paths; state is written under `.agent-team//authoring`; `source` may instead carry an inline, flat-team, AgentArtifact, or draft.1 migration input | +| `uar-workspace-migrate` | Same request as workspace initialization, with `source` or a contained compiled `sourceDirectory`; writes a field-level migration receipt beside the source documents | +| `uar-workspace-status` | `project`, `workspace`, optional numeric `cursor` and `pageSize` from 1 through 50 | +| `uar-workspace-answer` | `project`, team ID in `workspace`, current `expectedRevision`, stable `questionId`, and one `answer` value | +| `uar-workspace-update` | `project`, team ID in `workspace`, current `expectedRevision`, one declared relative `path`, and one identity-preserving `document` object | +| `uar-workspace-revise` | `project`, source and destination team IDs in `workspace` and `out`, current `expectedRevision`, strictly greater `nextVersion`, and optional explicit definition `edits` | +| `uar-package-schema-info` | Empty request; reports the accepted profile and provider source revision | +| `uar-package-validate` | Existing inline `package`, or `project` plus `workspace`; draft.1 inputs migrate explicitly | +| `uar-package-build` | Inline `package` or selected workspace, plus a new `out` directory | +| `uar-package-diff` | Inline `before`/`after`, compiled `beforeDirectory`/`afterDirectory`, or `project` with `beforeWorkspace`/`afterWorkspace` | | `uar-capabilities` | `connection` with base URL and optional `env:VARIABLE` credential reference | | `uar-package-preflight`, `uar-package-install` | `connection`, `packageDirectory`, `commandId`, optional `expectedCatalogRevision` | | `uar-package-status` | `connection`, `packageId`, `version` | | `uar-binding-preflight`, `uar-binding-install` | `connection`, `commandId`, complete `binding`, optional `expectedRevision` | | `uar-binding-status` | `connection`, `bindingId`, explicit `workspaceId` | -| `uar-activate` | Always refuses until durable local-team execution is implemented in I2 | +| `uar-activate` | Always refuses; the accepted draft.2 checkpoint does not claim durable team runtime conformance | Native harness exports are proposals, never in-place installation. If a write fails partway, the incomplete directory remains inspectable and lacks its final receipt. Choose a new output directory for a retry. UAR collaboration commands are the deliberate exception: after capability and package preflight they install an atomic immutable catalog package and optional private deployment binding through the routes in -`uar-deployment.md`. They never activate or execute a team. +`uar-deployment.md`. Package installation confers no private authority. They never +activate or execute a team. ## Building and distributing diff --git a/skills/agent-team-creator/references/uar-deployment.md b/skills/agent-team-creator/references/uar-deployment.md index dc2eb0e..030d0e1 100644 --- a/skills/agent-team-creator/references/uar-deployment.md +++ b/skills/agent-team-creator/references/uar-deployment.md @@ -1,88 +1,121 @@ -# UAR collaboration packages and deployment - -This skill implements the catalog portion of the official UAR collaboration Draft -0.1.0-draft.1. The canonical schemas are copied under `schemas/uar/`; the -authoring envelope is `schemas/uar-package-authoring.schema.json`. Catalog -installation does **not** create a TeamInstance, schedule a task, start a run, or -grant authority. Durable team activation belongs to implementation phase I2. - -## Authoring an immutable package - -An authoring request contains `manifest` and `definitions`. Definitions are -complete AgentDefinition, TeamDefinition, or WorkflowDefinition documents with -their top-level `contentDigest` omitted. Immutable references may omit `digest` -while authoring, but must include exact `id` and `version`. The compiler: - -1. resolves every permitted child, team member, and allowed workflow inside the - same package; -2. rejects dependency cycles, unresolved identities, and conflicting supplied - digests; -3. computes each definition digest from RFC 8785 canonical JSON with only the - top-level self digest omitted; -4. writes exact UTF-8 definition files with byte digests; -5. builds the PackageManifest file inventory and complete dependency lock; and -6. computes the package content digest. - -The author supplies every required semantic field from the canonical schemas. -That includes complete skill ID/version/digest/required/config values, model -capability requirements, context selection and requested limits. A copied skill -name or a preferred model string is not an immutable UAR dependency. +# UAR draft.2 authoring and deployment -```text -node /scripts/cli.mjs uar-package-validate --input package-request.json -node /scripts/cli.mjs uar-package-build --input package-request.json -``` +This skill consumes the UAR collaboration profile +`urn:prometheus:uar:collaboration:0.1.0-draft.2` from the immutable provider commit +recorded in `schemas/uar/0.1.0-draft.2/consumer-source-receipt.json`. The versioned schemas under +`schemas/uar/0.1.0-draft.2/` are byte-identical provider data. Draft.1 schemas +and inline authoring remain available for explicit migration and are never +rewritten or relabeled. -`uar-package-build` also requires `"out": ""`. It writes -`manifest.json` last. Existing output directories are refused. Live preflight -and install preserve that exact manifest UTF-8 string and send definition files -as an exact `{path: contentUtf8}` map; they never reserialize reviewed bytes. +Schema validity establishes document shape. It does not establish durable team +execution, current policy, a current representation grant, private credential +validity, or activation. -## Versioned maintenance +## File-backed authoring -Treat an installed package version as immutable. Create a replacement authoring -request with the same package ID and a new semantic version, then compare it -before building: +Initialize a contained project workspace from `assets/uar-intake.json`: ```text -node /scripts/cli.mjs guide --input revise-intake.json -node /scripts/cli.mjs uar-package-diff --input package-diff.json +node /scripts/cli.mjs uar-workspace-init --input uar-intake.json ``` -The diff request is `{"before": , "after": -}`. It reports added, removed, and JSON-pointer changed fields. -Changed content under the same package version is refused. Removing or changing a -member does not rewrite runtime history; active membership drain/cancel decisions -belong to I2 administration. - -## Connection and authentication +The `workspace` request field is a portable team ID. The creator owns only +`.agent-team//authoring`, and initialization requires +`expectedRevision: 0`. The resulting `workspace.json` persists a monotonic +revision, stable guided-question nodes and accepted answers, and identifies one +`manifest.source.json` plus separate +files such as `agents/coordinator.json`, `teams/root.json`, +`teams/review-subteam.json`, and `workflows/checkout.json`. Paths are relative, +case-insensitively unique, and confined beneath the selected project. Source +writes use same-directory atomic replacement and the existing +`.agent-team/recovery/` receipts. + +Use one answer or document per update. Both require the current +`expectedRevision`; stale revisions fail before any file is written. The response +identifies the prior and current revision plus the changed question or document: -Every live command accepts: - -```json -{ - "connection": { - "baseUrl": "http://127.0.0.1:1906", - "credentialRef": "env:UAR_TOKEN" - } -} +```text +node /scripts/cli.mjs uar-workspace-update --input update-one-document.json +node /scripts/cli.mjs uar-workspace-answer --input answer-one-question.json +node /scripts/cli.mjs uar-workspace-status --input workspace-status.json ``` -Only `env:VARIABLE` credential references are accepted. The secret is read for -the current request, sent as a Bearer token, and never included in request JSON, -receipts, package files, bindings, errors, or command output. A base URL cannot -contain user information, query parameters, or fragments. - -## Capability, package, and binding lifecycle - -Use the operations in order: - -I1 advertises `collaboration_definition_packages_v1` and -`collaboration_deployment_bindings_v1`. Require only the capability used by -the requested operation. Neither capability implies durable team execution. -The authenticated capability response also returns `bindingOwnerId`; copy that -opaque value into `DeploymentBinding.ownerId`. Do not derive an owner ID from a -token subject or from the runtime's private persistence-key format. +Status returns fixed counts, one dependency-ordered question, and at most 50 +field diagnostics. Supply its numeric continuation cursor to read the next page. +It does not embed the complete definition graph. The bundled +`assets/uar-workspace/` directory demonstrates a root team, several agents, a +permitted child, a nested subteam, and a workflow. + +## Migration and validation + +`uar-workspace-migrate` accepts the existing inline envelope, a schema-v1 flat +team, a legacy UAR AgentArtifact, or draft.1 package source. It preserves the +source as non-executable migration material after private-content checks and emits +field diagnostics with one of these dispositions: + +- `exact`: the field retained its meaning and target value; +- `translated`: an explicit profile or carrier translation retained it; +- `optional-unsupported`: the original value remains inspectable but is not + effective runtime behavior; +- `required-unsupported`: the requested semantics cannot be enforced, so build + and deployment preflight refuse. + +Skill `id`, `version`, `digest`, `required`, `config`, `entrypoint`, and +`requiredTools` are independent fields. Draft.2 validation applies provider +schemas first, graph checks second, and support diagnostics last. A valid package +has exactly one TeamDefinition entrypoint; member kinds match referenced document +kinds; coordinators name agent roles; child references name agents; workflow +roles exist in each accepting team; dependency graphs are acyclic; and every +reference resolves the exact version and digest. + +Inline callers continue to use `uar-package-validate` and `uar-package-build` +with `package`. Workspace callers use the same commands with `project` and +`workspace`. Builds write a new immutable directory only. + +## Immutable maintenance + +Use `uar-workspace-revise` with `assets/revise-intake.json` to copy source into a +distinct, strictly greater semantic-version workspace. Supply explicitly edited +definition documents in `edits`; unchanged definitions retain their exact source +bytes and immutable tuples, while a changed dependency tuple propagates only +through definitions that reference it. The old workspace and compiled package do +not change. `uar-package-diff` compares two workspaces, two compiled directories, +or two inline envelopes by definition identity and JSON Pointer. Changed content +under the same package version is refused. + +Rollback selects an already installed earlier package through a new private +binding revision. It does not rewrite package bytes or reverse external effects. + +## Private authority boundary + +Portable source, migration receipts, and compiled packages contain no credential +value, credential reference, RepresentationGrant record, consent evidence, +EffectiveBindingReceipt, or installed authority. Recognized private material +produces a field-level refusal before package files are written. + +DeploymentBinding is separate private installed state. Its model credential, +storage connection, representation grant, and effective receipt fields contain +opaque host references only. UAR resolves current credentials, policy, grants, +and authority independently. Successful package installation alone confers none +of them. + +## Live package and binding sequence + +Every live command accepts an HTTP(S) base URL and optional +`env:VARIABLE` credential reference. The secret is read only for that request and +is never written into request JSON, source, package, binding output, or receipts. + +Use this order: + +1. `uar-capabilities` +2. `uar-package-preflight` +3. `uar-package-install` +4. `uar-package-status` +5. optional `uar-binding-preflight` +6. optional compare-and-swap `uar-binding-install` +7. `uar-binding-status` + +The accepted provider checkpoint does not change the existing versioned routes: | Command | UAR operation | |---|---| @@ -94,23 +127,16 @@ token subject or from the runtime's private persistence-key format. | `uar-binding-install` | `POST /api/v1/collaboration/deployment-bindings` | | `uar-binding-status` | `GET /api/v1/collaboration/deployment-bindings/{id}` with explicit workspace ID | -Package preflight/install requests name `packageDirectory`, `commandId`, and -optionally `expectedCatalogRevision`. The client re-verifies file and canonical -digests before sending `{commandId, expectedCatalogRevision?, manifest: -, files: {: }}`. -Install all package definitions atomically; do not fall back to a sequence of -legacy `POST /api/agents` calls. - -Binding preflight/install requests carry `commandId`, optional -`expectedRevision`, and the complete approved DeploymentBinding. The compiler -fills or verifies its content digest and sends `binding.workspaceId` as -`x-uar-workspace-id`. Binding status requires an explicit `workspaceId` input -and sends the same header. Model credentials, storage connections, and other -private resources appear only as protected host references. A binding authorizes -nothing by itself; UAR derives the authenticated owner and evaluates current -policy. Structured failures read `error.messageKey`, `error.code`, and -`error.detail` so operator recovery information is not discarded. - -All successful command results include `catalogOnly: true` and an activation -refusal. `uar-activate` always fails with the I2 boundary. This prevents a -catalog receipt from being mistaken for a running team. +Package requests send the exact reviewed manifest string and definition byte map. +Binding requests send `x-uar-workspace-id` and an optional expected revision for +compare-and-swap. `uar-activate` always refuses because this authoring client owns +definitions and deployment rather than execution. Use The Boss or another +authorized host supporting the selected runtime's negotiated execution profile. + +## Shared team instructions and cooperating-pair deployment + +TeamDefinition may carry `instructions: {revision, digest, text}`. The revision is a positive safe integer, digest is SHA-256 of the exact UTF-8 text, and text is nonempty and at most 16384 UTF-8 bytes. Omit the object when no shared guidance is desired; null is not an omission. Shared guidance is below immutable host policy and above member specialization/task instructions. It cannot expand policy, credentials or resource grants. Peer messages, task input and artifacts remain attributed untrusted data. + +Changing guidance requires a new immutable definition/package version and a revisioned private binding. Existing attempts retain captured evidence; do not rewrite history. Preserve exact member skill ID/version/digest/config/required/entrypoint/tool requirements through authoring, maintenance and export. Required unsupported context/history/memory/child declarations refuse runtime admission with field diagnostics; optional exclusions must remain visible. A nested definition graph is not proof of nested team execution. + +For cooperation, explicitly install coordinator-to-worker trigger-turn and worker-to-coordinator queue-only result-disclosure edges. Sending queues a message; delegation explicitly requests work; waiting yields the live turn and creates a separate governed continuation. Do not infer reverse permission or automatic activation from a package receipt. Discover the execution profile and its peer-tools/shared-instructions/continuations capabilities before offering those runtime actions. Package/binding installation remains separate from activation, and this creator does not schedule agents. diff --git a/skills/agent-team-creator/runtime/src/cli.mts b/skills/agent-team-creator/runtime/src/cli.mts index 60d1683..fbfe725 100644 --- a/skills/agent-team-creator/runtime/src/cli.mts +++ b/skills/agent-team-creator/runtime/src/cli.mts @@ -10,7 +10,7 @@ import { readState, initState, mutateState, mutateStateAsync, taskAction, comple import { createHandoff, acceptHandoff } from './handoff.mjs'; import { discoverModels, selectModel } from './models.mjs'; import { queueMemory, publishMemory } from './memory.mjs'; -import { compileUarPackage, diffUarPackages, writeUarPackage } from './uar-package.mjs'; +import { dispatchUarAuthoring, isUarAuthoringCommand, uarAuthoringCommands } from './uar-package/commands.mjs'; import { refuseUarActivation, uarBindingInstall, uarBindingPreflight, uarBindingStatus, uarCapabilities, uarPackageInstall, uarPackagePreflight, uarPackageStatus, @@ -25,6 +25,7 @@ function revision(input: ObjectValue): number { const stateFile = (input: ObjectValue): string => path.resolve(text(input.state, 'state')); async function dispatch(command: string, input: ObjectValue): Promise { + if (isUarAuthoringCommand(command)) return dispatchUarAuthoring(command, input); switch (command) { case 'guide': return guide(input); case 'validate': return { valid: true, team: validateTeam(input.team) }; @@ -64,9 +65,6 @@ async function dispatch(command: string, input: ObjectValue): Promise { const state = await mutateStateAsync(stateFile(input), revision(input), async state => { receipt = await publishMemory(state, object(input.publication, 'publication')); }); return { state, publication: receipt }; } - case 'uar-package-validate': return { valid: true, package: compileUarPackage(input.package) }; - case 'uar-package-build': return writeUarPackage(input.out, input.package); - case 'uar-package-diff': return diffUarPackages(input.before, input.after); case 'uar-capabilities': return uarCapabilities(input); case 'uar-package-preflight': return uarPackagePreflight(input); case 'uar-package-install': return uarPackageInstall(input); @@ -79,7 +77,7 @@ async function dispatch(command: string, input: ObjectValue): Promise { } } -const commands = ['guide','validate','init','status','team-update','export','install-project','task','complete-kbd','handoff-create','handoff-accept','models-discover','models-select','memory-queue','memory-publish','uar-package-validate','uar-package-build','uar-package-diff','uar-capabilities','uar-package-preflight','uar-package-install','uar-package-status','uar-binding-preflight','uar-binding-install','uar-binding-status','uar-activate']; +const commands = ['guide','validate','init','status','team-update','export','install-project','task','complete-kbd','handoff-create','handoff-accept','models-discover','models-select','memory-queue','memory-publish',...uarAuthoringCommands,'uar-capabilities','uar-package-preflight','uar-package-install','uar-package-status','uar-binding-preflight','uar-binding-install','uar-binding-status','uar-activate']; async function main(): Promise { if (Number(process.versions.node.split('.')[0]) < 22) throw Error('Node.js 22 or newer is required'); const [command, ...args] = process.argv.slice(2); diff --git a/skills/agent-team-creator/runtime/src/guidance.mts b/skills/agent-team-creator/runtime/src/guidance.mts index 9d1e688..cb1cb39 100644 --- a/skills/agent-team-creator/runtime/src/guidance.mts +++ b/skills/agent-team-creator/runtime/src/guidance.mts @@ -1,9 +1,10 @@ -import type { Team, Role, ObjectValue } from './types.mjs'; +import type { Team, Role, ObjectValue, UarWorkspaceStatus } from './types.mjs'; import { id, text, strings, target, object } from './validation.mjs'; +import { workspaceStatus } from './uar-package/workspace.mjs'; -export interface GuideQuestion { key: string; question: string; choices?: string[] } +export interface GuideQuestion { key: string; question: string; choices?: string[]; document?: string; pointer?: string } export interface GuideResult { - operation: 'create' | 'revise' | 'deploy'; + operation: 'create' | 'author' | 'revise' | 'deploy'; questions: GuideQuestion[]; team?: Team; proposedRoles?: Role[]; @@ -14,9 +15,10 @@ export interface GuideResult { skillDiscovery?: string; maintenance?: ObjectValue; deployment?: ObjectValue; + workspace?: UarWorkspaceStatus; } export const questions: GuideQuestion[] = [ - { key: 'operation', question: 'Are you creating a team, revising a versioned team, or deploying an approved package?', choices: ['create', 'revise', 'deploy'] }, + { key: 'operation', question: 'Are you creating a local team, authoring a persisted UAR workspace, revising a versioned team, or deploying an approved package?', choices: ['create', 'author', 'revise', 'deploy'] }, { key: 'id', question: 'What short name should identify this team?' }, { key: 'outcome', question: 'What should be different when this work is finished?' }, { key: 'complexity', question: 'Is this one isolated change, or work spanning several components?', choices: ['simple', 'complex'] }, @@ -51,11 +53,17 @@ const specialists: Record input[key] === undefined); - if (missing.length) return { operation, ready: false, missing, questions: revisionQuestions }; + if (missing.length) return { operation, ready: false, missing: [missing[0]!], questions: revisionQuestions.filter(question => question.key === missing[0]) }; const nextVersion = text(input.nextVersion, 'nextVersion'); if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(nextVersion)) throw Error('nextVersion must be semantic version x.y.z'); if (!['local-only','stage','deploy'].includes(String(input.deploymentIntent))) throw Error('Invalid deploymentIntent'); @@ -69,7 +77,7 @@ export function guide(input: ObjectValue): GuideResult { if (operation === 'deploy') { const required = ['baseUrl', 'credentialRef', 'packageDirectory', 'bindingIntent']; const missing = required.filter(key => input[key] === undefined); - if (missing.length) return { operation, ready: false, missing, questions: deploymentQuestions }; + if (missing.length) return { operation, ready: false, missing: [missing[0]!], questions: deploymentQuestions.filter(question => question.key === missing[0]) }; if (!['package-only','package-and-binding'].includes(String(input.bindingIntent))) throw Error('Invalid bindingIntent'); const credentialRef = text(input.credentialRef, 'credentialRef'); if (!/^env:[A-Za-z_][A-Za-z0-9_]*$/.test(credentialRef)) throw Error('credentialRef must use env:VARIABLE'); @@ -77,11 +85,12 @@ export function guide(input: ObjectValue): GuideResult { connection: { baseUrl: text(input.baseUrl, 'baseUrl'), credentialRef }, packageDirectory: text(input.packageDirectory, 'packageDirectory'), bindingIntent: input.bindingIntent as string, sequence: ['uar-capabilities', 'uar-package-preflight', 'uar-package-install', ...(input.bindingIntent === 'package-and-binding' ? ['uar-binding-preflight', 'uar-binding-install'] : [])], - activation: 'refused-until-I2', + activation: 'not-owned-by-authoring-client', } }; } const required = ['id','outcome','complexity','areas','deliverables','budget','review','harness','scope']; const missing = required.filter(k => input[k] === undefined); + if (missing.length && input.scope === 'uar') return { operation, questions: questions.filter(question => question.key === missing[0]), missing: [missing[0]!] }; if (missing.length) return { operation, questions, missing }; const teamId = id(input.id), outcome = text(input.outcome, 'outcome'); const areas = strings(input.areas, 'areas'), deliverables = strings(input.deliverables, 'deliverables'); @@ -115,24 +124,33 @@ export function guide(input: ObjectValue): GuideResult { const unresolved = roles.filter(r => r.owns.length === 0); const alternatives = ['Use one implementer for sequential work; invoke specialist skills as needed.', 'Add parallel roles only where work and file ownership can be separated.']; const skillDiscovery = 'Skill names are suggestions, not installation claims. Discover installed AgentSkills, inspect their source and requirements, and replace or remove unavailable skills before export.'; - if (unresolved.length) return { operation, ready: false, proposedRoles: roles, reasons, alternatives, skillDiscovery, - missing: unresolved.map(r => 'ownership.' + r.id), - questions: unresolved.map(r => ({ key: 'ownership.' + r.id, question: 'Which project-relative files or output directories may ' + r.id + ' write? For read-only review, assign a separate findings path. Inspect the project and suggest paths instead of guessing.' })) }; + if (unresolved.length) { + const pending = input.scope === 'uar' ? unresolved.slice(0, 1) : unresolved; + return { operation, ready: false, proposedRoles: roles, reasons, alternatives, skillDiscovery, + missing: pending.map(r => 'ownership.' + r.id), + questions: pending.map(r => ({ key: 'ownership.' + r.id, question: 'Which project-relative files or output directories may ' + r.id + ' write? For read-only review, assign a separate findings path. Inspect the project and suggest paths instead of guessing.' })) }; + } const team: Team = { schemaVersion: 1, id: teamId, outcome, scope: input.scope as Team['scope'], harness, roles, modelPolicy: { tier } }; if (input.scope === 'uar') { - const required = ['packageId','packageVersion','coordinatorRole','workflowSummary','communicationPolicy','aggregateLimits','aggregateBudget','bindingIntent']; + const required = ['packageId','packageVersion','coordinatorRole','workflowSummary','communicationPolicy','aggregateLimits','aggregateBudget','bindingIntent','sharedInstructions','requiredResources','executionProfile']; const missing = required.filter(key => input[key] === undefined); const uarQuestions: GuideQuestion[] = [ { key: 'packageId', question: 'What stable UAR package identity should own these agent, team, and workflow definitions?' }, { key: 'packageVersion', question: 'What semantic version identifies this immutable package?' }, { key: 'coordinatorRole', question: 'Which proposed role is the single fixed coordinator?' }, { key: 'workflowSummary', question: 'What finite task dependencies, outputs, effects, approvals, and retry decisions should the workflow declare?' }, - { key: 'communicationPolicy', question: 'Which explicit role-to-role paths may queue a message or trigger a turn?' }, + { key: 'communicationPolicy', question: 'Which directed role-to-role edges permit queue-only messages, explicit delegation, and worker-to-coordinator result disclosure? Reply permission requires its own reverse edge.' }, + { key: 'sharedInstructions', question: 'What shared behavioral instructions should every member receive? Use an empty string for none; this guidance cannot expand tools, credentials, policy, or resource grants.' }, + { key: 'requiredResources', question: 'Which exact skills/tools and selected inputs are required by each role, and which KB, history, or memory requests may be excluded? Unsupported required resources block runtime admission.' }, + { key: 'executionProfile', question: 'Is this package for catalog-only authoring, manual member execution, or the negotiated cooperating-pair execution profile?', choices: ['catalog-only', 'manual-member', 'cooperating-pair'] }, { key: 'aggregateLimits', question: 'What aggregate concurrent-turn, member, nesting, and pending-task limits should the team request?' }, { key: 'aggregateBudget', question: 'What aggregate token, cost, currency, and elapsed-time ceilings should the team request?' }, { key: 'bindingIntent', question: 'Should creation stop after the immutable catalog package or also prepare a private deployment binding?', choices: ['package-only', 'package-and-binding'] }, ]; - if (missing.length) return { operation, ready: false, proposedRoles: roles, reasons, alternatives, skillDiscovery, missing, questions: uarQuestions }; + if (missing.length) return { operation, ready: false, proposedRoles: roles, reasons, alternatives, skillDiscovery, missing: [missing[0]!], questions: uarQuestions.filter(question => question.key === missing[0]) }; + if (typeof input.sharedInstructions !== 'string' || Buffer.byteLength(input.sharedInstructions, 'utf8') > 16_384) throw Error('sharedInstructions must be a string of at most 16384 UTF-8 bytes; use an empty string for none'); + if (!['catalog-only','manual-member','cooperating-pair'].includes(String(input.executionProfile))) throw Error('Invalid executionProfile'); + const resources = object(input.requiredResources, 'requiredResources'); const packageVersion = text(input.packageVersion, 'packageVersion'); if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(packageVersion)) throw Error('packageVersion must be semantic version x.y.z'); const coordinatorRole = id(input.coordinatorRole, 'coordinatorRole'); @@ -141,8 +159,10 @@ export function guide(input: ObjectValue): GuideResult { return { operation, ready: true, questions: [], team, reasons, alternatives, skillDiscovery, maintenance: { packageId: text(input.packageId, 'packageId'), packageVersion, coordinatorRole, workflowSummary: text(input.workflowSummary, 'workflowSummary'), communicationPolicy: text(input.communicationPolicy, 'communicationPolicy'), + sharedInstructions: input.sharedInstructions, requiredResources: resources, executionProfile: input.executionProfile as string, + executionAuthority: 'Catalog authoring never activates members or widens private binding authority. Negotiate runtime capabilities before offering automatic cooperation.', limits: object(input.aggregateLimits, 'aggregateLimits'), budget: object(input.aggregateBudget, 'aggregateBudget'), bindingIntent: input.bindingIntent as string, - next: 'Author complete canonical documents with exact skill locks, model aliases, input/output contracts, and required capabilities, then run uar-package-validate.', + next: 'Initialize a persisted workspace, update one declared source document at a time, and use operation=author for one bounded next question.', } }; } return { operation, ready: true, questions: [], team, reasons, diff --git a/skills/agent-team-creator/runtime/src/project-files.mts b/skills/agent-team-creator/runtime/src/project-files.mts index c41dae7..c9a1d3c 100644 --- a/skills/agent-team-creator/runtime/src/project-files.mts +++ b/skills/agent-team-creator/runtime/src/project-files.mts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import { createHash } from 'node:crypto'; +import { createHash, randomUUID } from 'node:crypto'; import { relativeFile } from './validation.mjs'; export interface Change { file: string; absolute: string; before: string | null; after: string } @@ -35,29 +35,47 @@ export function stage(changes: Map, root: string, file: string, if (before !== content) changes.set(absolute, { file, absolute, before, after: content }); } -export function commitChanges(root: string, changes: Change[]): string | null { +function atomicReplace(file: string, content: string): void { + const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${randomUUID()}.tmp`); + const descriptor = fs.openSync(temporary, 'wx', 0o600); + try { fs.writeFileSync(descriptor, content); fs.fsyncSync(descriptor); } + catch (error) { fs.closeSync(descriptor); fs.rmSync(temporary, { force: true }); throw error; } + fs.closeSync(descriptor); + try { + for (let attempt = 0; ; attempt++) { + try { fs.renameSync(temporary, file); break; } + catch (error) { + const transient = ['EPERM', 'EBUSY', 'EACCES'].includes((error as NodeJS.ErrnoException).code ?? ''); + if (process.platform !== 'win32' || !transient || attempt >= 6) throw error; + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25 * 2 ** attempt); + } + } + } finally { fs.rmSync(temporary, { force: true }); } +} + +export function commitChanges(root: string, changes: Change[], context?: Record): string | null { if (!changes.length) return null; const receiptId = hash(JSON.stringify(changes.map(c => [c.file, c.before, c.after]))).slice(0, 24); const recovery = `.agent-team/recovery/${receiptId}.json`; const recoveryFile = projectFile(root, recovery); // Recovery is inspectable and contains exact previous bytes before the first edit. - const receipt = JSON.stringify({ schemaVersion: 1, files: changes.map(c => ({ file: path.relative(root, c.absolute).split(path.sep).join('/'), before: c.before, afterSha256: hash(c.after) })) }, null, 2) + '\n'; + const receipt = JSON.stringify({ schemaVersion: 1, ...(context ? { context } : {}), files: changes.map(c => ({ file: path.relative(root, c.absolute).split(path.sep).join('/'), before: c.before, afterSha256: hash(c.after) })) }, null, 2) + '\n'; const existing = readFile(recoveryFile); if (existing !== null && existing !== receipt) throw Error('Recovery receipt collision'); fs.mkdirSync(path.dirname(recoveryFile), { recursive: true }); - if (existing === null) fs.writeFileSync(recoveryFile, receipt, { flag: 'wx' }); + if (existing === null) atomicReplace(recoveryFile, receipt); const written: Change[] = []; try { for (const change of changes) { if (readFile(change.absolute) !== change.before) throw Error(`Project changed after preflight: ${change.file}`); fs.mkdirSync(path.dirname(change.absolute), { recursive: true }); - fs.writeFileSync(change.absolute, change.after); + atomicReplace(change.absolute, change.after); written.push(change); } } catch (error) { for (const change of written.reverse()) { if (change.before === null) fs.unlinkSync(change.absolute); - else fs.writeFileSync(change.absolute, change.before); + else atomicReplace(change.absolute, change.before); } throw error; } diff --git a/skills/agent-team-creator/runtime/src/types.mts b/skills/agent-team-creator/runtime/src/types.mts index 98b0123..3801cc2 100644 --- a/skills/agent-team-creator/runtime/src/types.mts +++ b/skills/agent-team-creator/runtime/src/types.mts @@ -118,3 +118,73 @@ export interface UarConnection { baseUrl: string; credentialRef?: string; } +export const UAR_PROFILE_V1 = 'urn:prometheus:uar:collaboration:0.1.0-draft.1' as const; +export const UAR_PROFILE_V2 = 'urn:prometheus:uar:collaboration:0.1.0-draft.2' as const; +export type UarProfile = typeof UAR_PROFILE_V1 | typeof UAR_PROFILE_V2; +export type MigrationDisposition = 'exact' | 'translated' | 'optional-unsupported' | 'required-unsupported'; +export interface MigrationDiagnostic { + sourceDocument: string; + sourcePointer: string; + disposition: MigrationDisposition; + targetDocument?: string; + targetPointer?: string; + reason: string; +} +export interface UarMigrationReceipt { + schemaVersion: 1; + sourceProfile: string; + targetProfile: typeof UAR_PROFILE_V2; + diagnostics: MigrationDiagnostic[]; + activationBlocked: boolean; + preservedSource?: Json; +} +export interface UarWorkspaceIndex { + schemaVersion: 1; + revision: number; + profile: typeof UAR_PROFILE_V2; + teamId: string; + packageId: string; + packageVersion: string; + manifest: string; + definitions: string[]; + migrationReceipt?: string; + bindingIntent?: 'package-only' | 'package-and-binding'; + base?: { teamId: string; revision: number; packageVersion: string }; + questionState: UarQuestionState; +} +export interface UarQuestionNode { + id: string; + document: string; + pointer: string; + prompt: string; + required: boolean; + state: 'pending' | 'answered'; + answer?: Json; +} +export interface UarQuestionState { + currentQuestionId: string | null; + questions: UarQuestionNode[]; +} +export interface UarWorkspace { + root: string; + index: UarWorkspaceIndex; + package: UarAuthoringPackage; + migrationReceipt?: UarMigrationReceipt; +} +export interface UarDiagnosticPage { + items: MigrationDiagnostic[]; + cursor: string | null; + remaining: number; +} +export interface UarWorkspaceStatus { + teamId: string; + revision: number; + packageId: string; + packageVersion: string; + profile: typeof UAR_PROFILE_V2; + counts: { agents: number; teams: number; workflows: number; diagnostics: number }; + complete: boolean; + nextQuestion: { id: string; document: string; pointer: string; question: string } | null; + questions: { answered: number; pending: number; currentQuestionId: string | null }; + diagnostics: UarDiagnosticPage; +} diff --git a/skills/agent-team-creator/runtime/src/uar-client.mts b/skills/agent-team-creator/runtime/src/uar-client.mts index 77774d3..1b5d0a7 100644 --- a/skills/agent-team-creator/runtime/src/uar-client.mts +++ b/skills/agent-team-creator/runtime/src/uar-client.mts @@ -49,15 +49,19 @@ async function request(connectionValue: unknown, method: 'GET' | 'POST', route: return payload; } -function catalogOnly(operation: string, response: unknown): ObjectValue { +function collaborationBoundary(operation: string, response: unknown, binding = false): ObjectValue { return { operation, response: response as never, - catalogOnly: true, + profile: 'urn:prometheus:uar:collaboration:0.1.0-draft.2', + catalogOnly: !binding, + authority: { + conferredByPackageInstallation: false, + privateBindingEvaluatedByUar: binding, + }, activation: { supported: false, - reason: 'Team activation is unavailable until the durable local-team I2 runtime is implemented.', - nextPhase: 'I2', + reason: 'This authoring client installs definitions and private bindings; runtime activation belongs to the selected UAR instance and its negotiated execution profile.', }, }; } @@ -84,7 +88,7 @@ export async function uarPackagePreflight(input: ObjectValue): Promise [file.path, file.contentUtf8])), }); - return catalogOnly('package-preflight', response); + return collaborationBoundary('package-preflight', response); } export async function uarPackageInstall(input: ObjectValue): Promise { @@ -93,14 +97,14 @@ export async function uarPackageInstall(input: ObjectValue): Promise [file.path, file.contentUtf8])), }); - return catalogOnly('package-install', response); + return collaborationBoundary('package-install', response); } export async function uarPackageStatus(input: ObjectValue): Promise { const id = encodeURIComponent(text(input.packageId, 'packageId')); const version = encodeURIComponent(text(input.version, 'version')); const response = await request(input.connection, 'GET', `/api/v1/collaboration/packages/${id}/versions/${version}`); - return catalogOnly('package-status', response); + return collaborationBoundary('package-status', response); } function bindingRequest(input: ObjectValue): { body: ObjectValue; workspaceId: string } { @@ -121,22 +125,22 @@ function bindingRequest(input: ObjectValue): { body: ObjectValue; workspaceId: s export async function uarBindingPreflight(input: ObjectValue): Promise { const { body, workspaceId } = bindingRequest(input); const response = await request(input.connection, 'POST', '/api/v1/collaboration/deployment-bindings:preflight', body, { 'x-uar-workspace-id': workspaceId }); - return catalogOnly('binding-preflight', response); + return collaborationBoundary('binding-preflight', response, true); } export async function uarBindingInstall(input: ObjectValue): Promise { const { body, workspaceId } = bindingRequest(input); const response = await request(input.connection, 'POST', '/api/v1/collaboration/deployment-bindings', body, { 'x-uar-workspace-id': workspaceId }); - return catalogOnly('binding-install', response); + return collaborationBoundary('binding-install', response, true); } export async function uarBindingStatus(input: ObjectValue): Promise { const id = encodeURIComponent(text(input.bindingId, 'bindingId')); const workspaceId = text(input.workspaceId, 'workspaceId'); const response = await request(input.connection, 'GET', `/api/v1/collaboration/deployment-bindings/${id}`, undefined, { 'x-uar-workspace-id': workspaceId }); - return catalogOnly('binding-status', response); + return collaborationBoundary('binding-status', response, true); } export function refuseUarActivation(): never { - throw new Error('UAR team activation is not implemented in I1. Install definitions and an inactive or ready binding, then wait for the durable local-team I2 runtime.'); + throw new Error('This authoring client does not invoke team activation. Use the selected UAR instance through a host supporting its negotiated execution profile.'); } diff --git a/skills/agent-team-creator/runtime/src/uar-package.mts b/skills/agent-team-creator/runtime/src/uar-package.mts index 8d3a5df..e677de2 100644 --- a/skills/agent-team-creator/runtime/src/uar-package.mts +++ b/skills/agent-team-creator/runtime/src/uar-package.mts @@ -1,434 +1,7 @@ -import { createHash } from 'node:crypto'; -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import type { - Json, ObjectValue, UarAuthoringDefinition, - UarCompiledPackage, UarDefinitionKind, -} from './types.mjs'; -import { object, relativeFile, text } from './validation.mjs'; - -const PROFILE = 'urn:prometheus:uar:collaboration:0.1.0-draft.1'; -const DIGEST = /^sha256:[a-f0-9]{64}$/; -const SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/; -const kinds = new Set(['AgentDefinition', 'TeamDefinition', 'WorkflowDefinition']); -const requiredByKind: Record = { - AgentDefinition: ['provenance','requiredCapabilities','extensions','title','role','whenToUse','instructions','input','output','skills','models','permittedChildren','context','requestedLimits'], - TeamDefinition: ['provenance','requiredCapabilities','extensions','title','purpose','members','coordinatorRole','communication','taskAcceptance','routing','limits','budget','input','output'], - WorkflowDefinition: ['provenance','requiredCapabilities','extensions','title','input','output','steps','failurePolicy','maxActivations'], -}; -const allowedByKind: Record> = { - AgentDefinition: new Set(['profile','kind','id','version','contentDigest',...requiredByKind.AgentDefinition,'legacySections','sourceDescriptor']), - TeamDefinition: new Set(['profile','kind','id','version','contentDigest',...requiredByKind.TeamDefinition]), - WorkflowDefinition: new Set(['profile','kind','id','version','contentDigest',...requiredByKind.WorkflowDefinition]), -}; - -function sha256(value: string): string { - return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`; -} - -function assertUnicode(value: string): void { - for (let index = 0; index < value.length; index++) { - const code = value.charCodeAt(index); - if (code >= 0xd800 && code <= 0xdbff) { - const next = value.charCodeAt(index + 1); - if (next < 0xdc00 || next > 0xdfff) throw new Error('Canonical JSON refuses an unpaired high surrogate'); - index++; - } else if (code >= 0xdc00 && code <= 0xdfff) { - throw new Error('Canonical JSON refuses an unpaired low surrogate'); - } - } -} - -function canonical(value: Json): string { - if (value === null || typeof value === 'boolean') return JSON.stringify(value); - if (typeof value === 'number') { - if (!Number.isFinite(value)) throw new Error('Canonical JSON requires finite numbers'); - return JSON.stringify(value); - } - if (typeof value === 'string') { - assertUnicode(value); - return JSON.stringify(value); - } - if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`; - return `{${Object.keys(value).sort().map(key => { - assertUnicode(key); - return `${JSON.stringify(key)}:${canonical(value[key])}`; - }).join(',')}}`; -} - -function cloneObject(value: unknown, label: string): ObjectValue { - return structuredClone(object(value, label)); -} - -function selfDigest(document: ObjectValue): string { - const copy = structuredClone(document); - delete copy.contentDigest; - return sha256(canonical(copy)); -} - -function validateDefinitionSemantics(document: ObjectValue, kind: UarDefinitionKind): void { - if (kind === 'AgentDefinition') { - if (!Array.isArray(document.skills)) throw new Error('AgentDefinition.skills must be an array'); - for (const [index, entry] of document.skills.entries()) { - const skill = object(entry, `skills[${index}]`); - for (const field of ['id','version','digest']) text(skill[field], `skills[${index}].${field}`); - if (!DIGEST.test(String(skill.digest))) throw new Error(`skills[${index}].digest must be sha256:<64 lowercase hex>`); - if (typeof skill.required !== 'boolean') throw new Error(`skills[${index}].required must be boolean`); - object(skill.config, `skills[${index}].config`); - } - if (!Array.isArray(document.models) || document.models.length === 0) throw new Error('AgentDefinition.models must be a nonempty array'); - object(document.context, 'AgentDefinition.context'); - object(document.requestedLimits, 'AgentDefinition.requestedLimits'); - return; - } - if (kind === 'TeamDefinition') { - if (!Array.isArray(document.members) || document.members.length === 0) throw new Error('TeamDefinition.members must be a nonempty array'); - const roles = new Set(); - let coordinatorIsAgent = false; - for (const [index, entry] of document.members.entries()) { - const member = object(entry, `members[${index}]`); - const role = text(member.role, `members[${index}].role`); - if (roles.has(role)) throw new Error(`Duplicate team member role ${role}`); - roles.add(role); - if (!['agent','team'].includes(String(member.kind))) throw new Error(`Invalid members[${index}].kind`); - if (!Number.isSafeInteger(member.min) || !Number.isSafeInteger(member.max) || Number(member.min) < 0 || Number(member.max) < 1 || Number(member.min) > Number(member.max)) { - throw new Error(`Invalid members[${index}] cardinality`); - } - if (role === document.coordinatorRole && member.kind === 'agent') coordinatorIsAgent = true; - } - if (!coordinatorIsAgent) throw new Error('coordinatorRole must name an agent member'); - if (!Array.isArray(document.communication)) throw new Error('TeamDefinition.communication must be an array'); - for (const [index, entry] of document.communication.entries()) { - const edge = object(entry, `communication[${index}]`); - for (const field of ['fromRole','toRole']) if (!roles.has(text(edge[field], `communication[${index}].${field}`))) { - throw new Error(`communication[${index}].${field} is not a member role`); - } - } - return; - } - if (!Array.isArray(document.steps) || document.steps.length === 0) throw new Error('WorkflowDefinition.steps must be a nonempty array'); - const steps = new Map(); - for (const [index, entry] of document.steps.entries()) { - const step = object(entry, `steps[${index}]`); - const stepId = text(step.id, `steps[${index}].id`); - if (steps.has(stepId)) throw new Error(`Duplicate workflow step ${stepId}`); - if (!Array.isArray(step.dependsOn)) throw new Error(`steps[${index}].dependsOn must be an array`); - steps.set(stepId, step); - } - const visiting = new Set(), visited = new Set(); - const visit = (stepId: string): void => { - if (visiting.has(stepId)) throw new Error(`Workflow dependency cycle at ${stepId}`); - if (visited.has(stepId)) return; - const step = steps.get(stepId); - if (!step) throw new Error(`Unknown workflow dependency ${stepId}`); - visiting.add(stepId); - for (const dependency of step.dependsOn as Json[]) visit(text(dependency, `${stepId}.dependsOn`)); - visiting.delete(stepId); visited.add(stepId); - }; - for (const stepId of steps.keys()) visit(stepId); -} - -function definitionIdentity(document: ObjectValue, label: string): { id: string; version: string; kind: UarDefinitionKind } { - if (document.profile !== PROFILE) throw new Error(`${label}.profile must be ${PROFILE}`); - const kind = text(document.kind, `${label}.kind`) as UarDefinitionKind; - if (!kinds.has(kind)) throw new Error(`${label}.kind is not a collaboration definition`); - for (const field of Object.keys(document)) if (!allowedByKind[kind].has(field)) { - throw new Error(`${label} contains unknown ${kind} field ${field}`); - } - const id = text(document.id, `${label}.id`); - const version = text(document.version, `${label}.version`); - if (!SEMVER.test(version)) throw new Error(`${label}.version must be semantic version x.y.z`); - for (const field of requiredByKind[kind]) if (document[field] === undefined) { - throw new Error(`${label} is missing required ${kind} field ${field}`); - } - validateDefinitionSemantics(document, kind); - return { id, version, kind }; -} - -function references(document: ObjectValue): { owner: string; reference: ObjectValue }[] { - const owner = text(document.id, 'definition.id'); - const result: { owner: string; reference: ObjectValue }[] = []; - const add = (value: unknown, label: string): void => { - const reference = object(value, label); - text(reference.id, `${label}.id`); - const version = text(reference.version, `${label}.version`); - if (!SEMVER.test(version)) throw new Error(`${label}.version must be semantic version x.y.z`); - if (reference.digest !== undefined && !DIGEST.test(text(reference.digest, `${label}.digest`))) { - throw new Error(`${label}.digest must be sha256:<64 lowercase hex>`); - } - result.push({ owner, reference }); - }; - if (document.kind === 'AgentDefinition') { - const children = document.permittedChildren; - if (!Array.isArray(children)) throw new Error('AgentDefinition.permittedChildren must be an array'); - children.forEach((entry, index) => add(entry, `permittedChildren[${index}]`)); - } else if (document.kind === 'TeamDefinition') { - const members = document.members; - if (!Array.isArray(members)) throw new Error('TeamDefinition.members must be an array'); - members.forEach((entry, index) => add(object(entry, `members[${index}]`).definition, `members[${index}].definition`)); - const acceptance = object(document.taskAcceptance, 'taskAcceptance'); - const workflows = acceptance.allowedWorkflows; - if (!Array.isArray(workflows)) throw new Error('taskAcceptance.allowedWorkflows must be an array'); - workflows.forEach((entry, index) => add(entry, `taskAcceptance.allowedWorkflows[${index}]`)); - } - return result; -} - -function replaceReferences(document: ObjectValue, resolve: (reference: ObjectValue) => ObjectValue): void { - if (document.kind === 'AgentDefinition') { - document.permittedChildren = (document.permittedChildren as Json[]).map(item => resolve(object(item, 'permitted child'))); - } else if (document.kind === 'TeamDefinition') { - document.members = (document.members as Json[]).map(item => { - const member = cloneObject(item, 'team member'); - member.definition = resolve(object(member.definition, 'team member definition')); - return member; - }); - const acceptance = cloneObject(document.taskAcceptance, 'taskAcceptance'); - acceptance.allowedWorkflows = (acceptance.allowedWorkflows as Json[]).map(item => resolve(object(item, 'allowed workflow'))); - document.taskAcceptance = acceptance; - } -} - -export function compileUarPackage(value: unknown): UarCompiledPackage { - const source = object(value, 'authoring package'); - const manifestInput = cloneObject(source.manifest, 'package manifest'); - if (!Array.isArray(source.definitions) || source.definitions.length === 0) { - throw new Error('authoring package definitions must be a nonempty array'); - } - const definitions = (source.definitions as unknown[]).map((entry, index): UarAuthoringDefinition => { - const item = object(entry, `definitions[${index}]`); - const file = relativeFile(text(item.path, `definitions[${index}].path`)); - if (file === 'manifest.json') throw new Error('manifest.json is reserved for the compiled package manifest'); - return { path: file, document: cloneObject(item.document, `definitions[${index}].document`) }; - }); - const byKey = new Map(); - const byPath = new Set(); - for (const definition of definitions) { - const identity = definitionIdentity(definition.document, definition.path); - const key = `${identity.id}\u0000${identity.version}`; - if (byKey.has(key)) throw new Error(`Duplicate definition identity/version: ${identity.id}@${identity.version}`); - const foldedPath = definition.path.normalize('NFC').toLowerCase(); - if (byPath.has(foldedPath)) throw new Error(`Case-insensitive definition path collision: ${definition.path}`); - byKey.set(key, definition); - byPath.add(foldedPath); - } - - const compiled = new Map(); - const visiting = new Set(); - const compile = (key: string): UarAuthoringDefinition => { - const done = compiled.get(key); - if (done) return done; - if (visiting.has(key)) throw new Error(`Definition dependency cycle at ${key.replace('\u0000', '@')}`); - const sourceDefinition = byKey.get(key); - if (!sourceDefinition) throw new Error(`Unresolved definition reference ${key.replace('\u0000', '@')}`); - visiting.add(key); - const document = cloneObject(sourceDefinition.document, sourceDefinition.path); - const suppliedDigest = document.contentDigest; - delete document.contentDigest; - replaceReferences(document, reference => { - const referenceKey = `${text(reference.id, 'reference.id')}\u0000${text(reference.version, 'reference.version')}`; - const dependency = compile(referenceKey); - const digest = text(dependency.document.contentDigest, 'compiled dependency digest'); - if (reference.digest !== undefined && reference.digest !== digest) { - throw new Error(`Reference digest conflict for ${reference.id}@${reference.version}`); - } - return { id: reference.id, version: reference.version, digest }; - }); - const digest = selfDigest(document); - if (suppliedDigest !== undefined && suppliedDigest !== digest) { - throw new Error(`Definition contentDigest conflict for ${document.id}@${document.version}; remove the old digest before revising content`); - } - document.contentDigest = digest; - const result = { path: sourceDefinition.path, document }; - compiled.set(key, result); - visiting.delete(key); - return result; - }; - for (const key of byKey.keys()) compile(key); - - if (manifestInput.profile !== PROFILE || manifestInput.kind !== 'PackageManifest') { - throw new Error(`package manifest must use profile ${PROFILE} and kind PackageManifest`); - } - const manifestFields = new Set(['profile','kind','id','version','contentDigest','provenance','requiredCapabilities','extensions','entrypoints','files','lock','capabilityDeclarations','resolution']); - for (const field of Object.keys(manifestInput)) if (!manifestFields.has(field)) throw new Error(`package manifest contains unknown field ${field}`); - text(manifestInput.id, 'manifest.id'); - const manifestVersion = text(manifestInput.version, 'manifest.version'); - if (!SEMVER.test(manifestVersion)) throw new Error('manifest.version must be semantic version x.y.z'); - for (const field of ['provenance','requiredCapabilities','extensions','entrypoints','capabilityDeclarations']) { - if (manifestInput[field] === undefined) throw new Error(`package manifest is missing required field ${field}`); - } - if (manifestInput.resolution !== 'exact-version-and-digest') throw new Error('manifest.resolution must be exact-version-and-digest'); - const entrypoints = manifestInput.entrypoints; - if (!Array.isArray(entrypoints) || entrypoints.length === 0) throw new Error('manifest.entrypoints must be a nonempty array'); - manifestInput.entrypoints = entrypoints.map((entry, index) => { - const reference = object(entry, `manifest.entrypoints[${index}]`); - const key = `${text(reference.id, 'entrypoint.id')}\u0000${text(reference.version, 'entrypoint.version')}`; - const definition = compile(key); - const digest = text(definition.document.contentDigest, 'entrypoint digest'); - if (reference.digest !== undefined && reference.digest !== digest) throw new Error(`Entrypoint digest conflict for ${reference.id}@${reference.version}`); - return { id: reference.id, version: reference.version, digest }; - }); - - const files = [...compiled.values()].sort((a, b) => a.path.localeCompare(b.path)).map(definition => ({ - path: definition.path, - contentUtf8: `${JSON.stringify(definition.document, null, 2)}\n`, - })); - manifestInput.files = files.map(file => { - const definition = compiled.get(`${text(JSON.parse(file.contentUtf8).id, 'file id')}\u0000${text(JSON.parse(file.contentUtf8).version, 'file version')}`)!; - return { - path: file.path, - kind: definition.document.kind, - definition: { - id: definition.document.id, - version: definition.document.version, - digest: definition.document.contentDigest, - }, - byteDigest: sha256(file.contentUtf8), - }; - }); - const locks: ObjectValue[] = []; - for (const definition of compiled.values()) for (const item of references(definition.document)) { - const key = `${item.reference.id}\u0000${item.reference.version}`; - const resolved = compiled.get(key); - if (!resolved) throw new Error(`Unresolved compiled reference ${item.reference.id}@${item.reference.version}`); - locks.push({ - requestedBy: item.owner, - reference: item.reference, - resolvedPath: resolved.path, - }); - } - manifestInput.lock = locks.sort((a, b) => - String(a.requestedBy).localeCompare(String(b.requestedBy)) || - String(object(a.reference).id).localeCompare(String(object(b.reference).id))); - const suppliedManifestDigest = manifestInput.contentDigest; - delete manifestInput.contentDigest; - const digest = selfDigest(manifestInput); - if (suppliedManifestDigest !== undefined && suppliedManifestDigest !== digest) { - throw new Error('PackageManifest contentDigest conflict; remove the old digest before revising content'); - } - manifestInput.contentDigest = digest; - return { manifest: manifestInput, manifestUtf8: `${JSON.stringify(manifestInput, null, 2)}\n`, files }; -} - -export function compileUarBinding(value: unknown): ObjectValue { - const binding = cloneObject(value, 'deployment binding'); - if (binding.profile !== PROFILE || binding.kind !== 'DeploymentBinding') { - throw new Error(`deployment binding must use profile ${PROFILE} and kind DeploymentBinding`); - } - const required = [ - 'id','version','provenance','requiredCapabilities','extensions','exportClass','package','ownerId','workspaceId', - 'runtimeInstanceId','revision','modelBindings','skillBindings','storage','policyRevision','effectiveLimits', - 'effectiveBudget','contextGrants','representationGrantRefs','status', - ]; - const allowed = new Set(['profile','kind','contentDigest',...required]); - for (const field of Object.keys(binding)) if (!allowed.has(field)) throw new Error(`deployment binding contains unknown field ${field}`); - for (const field of required) if (binding[field] === undefined) throw new Error(`deployment binding is missing required field ${field}`); - if (binding.exportClass !== 'private-installed-state') throw new Error('deployment binding exportClass must be private-installed-state'); - if (!SEMVER.test(text(binding.version, 'binding.version'))) throw new Error('binding.version must be semantic version x.y.z'); - for (const field of ['id','ownerId','workspaceId','runtimeInstanceId','policyRevision']) text(binding[field], `binding.${field}`); - object(binding.provenance, 'binding.provenance'); - object(binding.extensions, 'binding.extensions'); - if (!['inactive','ready','suspended'].includes(String(binding.status))) throw new Error('Invalid deployment binding status'); - if (!Number.isSafeInteger(binding.revision) || Number(binding.revision) < 0) throw new Error('binding.revision must be a nonnegative safe integer'); - const packageRef = object(binding.package, 'binding.package'); - for (const field of ['id','version','digest']) text(packageRef[field], `binding.package.${field}`); - if (!DIGEST.test(String(packageRef.digest))) throw new Error('binding.package.digest must be sha256:<64 lowercase hex>'); - if (!Array.isArray(binding.modelBindings) || binding.modelBindings.length === 0) throw new Error('binding.modelBindings must be a nonempty array'); - for (const field of ['requiredCapabilities','skillBindings','contextGrants','representationGrantRefs']) { - if (!Array.isArray(binding[field])) throw new Error(`binding.${field} must be an array`); - } - for (const [index, entry] of binding.modelBindings.entries()) { - const model = object(entry, `binding.modelBindings[${index}]`); - for (const field of ['requestedAlias','providerId','modelId','credentialRef']) text(model[field], `binding.modelBindings[${index}].${field}`); - if (/^(?:bearer|token|secret|password|api[-_]?key):/i.test(String(model.credentialRef))) { - throw new Error(`binding.modelBindings[${index}].credentialRef must be an opaque host reference, not a credential value`); - } - } - const storage = object(binding.storage, 'binding.storage'); - for (const field of ['backend','connectionRef']) text(storage[field], `binding.storage.${field}`); - if (storage.durableTransactions !== true) throw new Error('binding.storage.durableTransactions must be true'); - object(binding.effectiveLimits, 'binding.effectiveLimits'); - object(binding.effectiveBudget, 'binding.effectiveBudget'); - const suppliedDigest = binding.contentDigest; - delete binding.contentDigest; - const digest = selfDigest(binding); - if (suppliedDigest !== undefined && suppliedDigest !== digest) throw new Error('DeploymentBinding contentDigest conflict'); - binding.contentDigest = digest; - return binding; -} - -export function writeUarPackage(directoryValue: unknown, value: unknown): { directory: string; manifest: ObjectValue; files: string[] } { - const compiled = compileUarPackage(value); - const directory = path.resolve(text(directoryValue, 'out')); - mkdirSync(path.dirname(directory), { recursive: true }); - mkdirSync(directory); - for (const file of compiled.files) { - const destination = path.join(directory, ...file.path.split('/')); - mkdirSync(path.dirname(destination), { recursive: true }); - writeFileSync(destination, file.contentUtf8, { flag: 'wx', mode: 0o600 }); - } - writeFileSync(path.join(directory, 'manifest.json'), compiled.manifestUtf8, { flag: 'wx', mode: 0o600 }); - return { directory, manifest: compiled.manifest, files: ['manifest.json', ...compiled.files.map(file => file.path)] }; -} - -export function loadUarPackage(directoryValue: unknown): UarCompiledPackage { - const directory = path.resolve(text(directoryValue, 'packageDirectory')); - const manifestUtf8 = readFileSync(path.join(directory, 'manifest.json'), 'utf8'); - const manifest = cloneObject(JSON.parse(manifestUtf8), 'manifest'); - if (!Array.isArray(manifest.files)) throw new Error('Compiled package manifest.files must be an array'); - const files = manifest.files.map((entry, index) => { - const item = object(entry, `manifest.files[${index}]`); - const file = relativeFile(text(item.path, `manifest.files[${index}].path`)); - const contentUtf8 = readFileSync(path.join(directory, ...file.split('/')), 'utf8'); - if (sha256(contentUtf8) !== item.byteDigest) throw new Error(`Package byte digest mismatch: ${file}`); - const document = object(JSON.parse(contentUtf8), file); - if (selfDigest(document) !== document.contentDigest) throw new Error(`Package content digest mismatch: ${file}`); - return { path: file, contentUtf8 }; - }); - if (selfDigest(manifest) !== manifest.contentDigest) throw new Error('Package manifest content digest mismatch'); - return { manifest, manifestUtf8, files }; -} - -function flatten(value: Json, prefix = ''): Map { - const result = new Map(); - const walk = (item: Json, current: string): void => { - if (item && typeof item === 'object') { - if (Array.isArray(item)) item.forEach((child, index) => walk(child, `${current}/${index}`)); - else Object.keys(item).sort().forEach(key => walk(item[key], `${current}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`)); - } else result.set(current || '/', canonical(item)); - }; - walk(value, prefix); - return result; -} - -export function diffUarPackages(beforeValue: unknown, afterValue: unknown): ObjectValue { - const before = compileUarPackage(beforeValue); - const after = compileUarPackage(afterValue); - const beforeFiles = new Map(before.files.map(file => [file.path, JSON.parse(file.contentUtf8) as Json])); - const afterFiles = new Map(after.files.map(file => [file.path, JSON.parse(file.contentUtf8) as Json])); - const added = [...afterFiles.keys()].filter(file => !beforeFiles.has(file)).sort(); - const removed = [...beforeFiles.keys()].filter(file => !afterFiles.has(file)).sort(); - const changes: ObjectValue[] = []; - for (const file of [...beforeFiles.keys()].filter(file => afterFiles.has(file)).sort()) { - const left = flatten(beforeFiles.get(file)!); - const right = flatten(afterFiles.get(file)!); - const paths = [...new Set([...left.keys(), ...right.keys()])].filter(key => left.get(key) !== right.get(key)).sort(); - if (paths.length) changes.push({ file, paths }); - } - const beforeId = text(before.manifest.id, 'before manifest id'); - const afterId = text(after.manifest.id, 'after manifest id'); - if (beforeId !== afterId) throw new Error('Package maintenance cannot change package identity'); - const sameVersion = before.manifest.version === after.manifest.version; - if (sameVersion && before.manifest.contentDigest !== after.manifest.contentDigest) { - throw new Error('Changed immutable package content requires a new semantic version'); - } - return { - packageId: beforeId, - fromVersion: before.manifest.version, - toVersion: after.manifest.version, - added, - removed, - changed: changes, - unchanged: before.manifest.contentDigest === after.manifest.contentDigest, - }; -} +export { compileUarPackage } from './uar-package/compiler.mjs'; +export { compileUarBinding } from './uar-package/binding.mjs'; +export { writeUarPackage, loadUarPackage, compiledAsAuthoring } from './uar-package/package-files.mjs'; +export { diffUarPackages, diffUarDirectories } from './uar-package/maintenance.mjs'; +export { normalizeAuthoring, assertMigrationAllowsBuild } from './uar-package/migration.mjs'; +export { initializeWorkspace, loadWorkspace, reviseWorkspace, updateWorkspaceDocument, workspaceStatus } from './uar-package/workspace.mjs'; +export { profileSchemaInfo, validateProfileDocument } from './uar-package/profile-validation.mjs'; diff --git a/skills/agent-team-creator/runtime/src/uar-package/binding.mts b/skills/agent-team-creator/runtime/src/uar-package/binding.mts new file mode 100644 index 0000000..93264b7 --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/binding.mts @@ -0,0 +1,34 @@ +import type { Json, ObjectValue } from '../types.mjs'; +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { object, text } from '../validation.mjs'; +import { cloneObject, selfDigest } from './canonical.mjs'; +import { assertBindingContainsReferencesOnly, assertOpaqueReference } from './private-authority.mjs'; +import { validateProfileDocument } from './profile-validation.mjs'; + +export function compileUarBinding(value: unknown): ObjectValue { + const binding = cloneObject(value, 'deployment binding'); + if (binding.profile !== UAR_PROFILE_V2 || binding.kind !== 'DeploymentBinding') throw new Error(`deployment binding must use profile ${UAR_PROFILE_V2} and kind DeploymentBinding`); + assertBindingContainsReferencesOnly(binding as Json); + const packageReference = object(binding.package, 'binding.package'); + for (const field of ['id','version','digest']) text(packageReference[field], `binding.package.${field}`); + if (!Array.isArray(binding.modelBindings) || binding.modelBindings.length === 0) throw new Error('binding.modelBindings must be a nonempty array'); + binding.modelBindings.forEach((entry, index) => { + const model = object(entry, `binding.modelBindings[${index}]`); + assertOpaqueReference(model.credentialRef, `/modelBindings/${index}/credentialRef`); + }); + const storage = object(binding.storage, 'binding.storage'); + assertOpaqueReference(storage.connectionRef, '/storage/connectionRef'); + if (!Array.isArray(binding.representationGrantRefs)) throw new Error('binding.representationGrantRefs must be an array'); + binding.representationGrantRefs.forEach((entry, index) => { + const reference = object(entry, `binding.representationGrantRefs[${index}]`); + text(reference.id, `binding.representationGrantRefs[${index}].id`); + if (!Number.isSafeInteger(reference.revision) || Number(reference.revision) < 0) throw new Error(`binding.representationGrantRefs[${index}].revision must be a nonnegative integer`); + }); + const supplied = binding.contentDigest; + delete binding.contentDigest; + const digest = selfDigest(binding); + if (supplied !== undefined && supplied !== digest) throw new Error('DeploymentBinding contentDigest conflict'); + binding.contentDigest = digest; + validateProfileDocument(binding); + return binding; +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/canonical.mts b/skills/agent-team-creator/runtime/src/uar-package/canonical.mts new file mode 100644 index 0000000..817ca8e --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/canonical.mts @@ -0,0 +1,61 @@ +import { createHash } from 'node:crypto'; +import type { Json, ObjectValue } from '../types.mjs'; +import { object } from '../validation.mjs'; + +export const DIGEST = /^sha256:[a-f0-9]{64}$/; +export const SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/; + +export function sha256(value: string | Uint8Array): string { + return `sha256:${createHash('sha256').update(value).digest('hex')}`; +} + +function assertUnicode(value: string): void { + for (let index = 0; index < value.length; index++) { + const code = value.charCodeAt(index); + if (code >= 0xd800 && code <= 0xdbff) { + const next = value.charCodeAt(index + 1); + if (next < 0xdc00 || next > 0xdfff) throw new Error('Canonical JSON refuses an unpaired high surrogate'); + index++; + } else if (code >= 0xdc00 && code <= 0xdfff) throw new Error('Canonical JSON refuses an unpaired low surrogate'); + } +} + +export function canonical(value: Json): string { + if (value === null || typeof value === 'boolean') return JSON.stringify(value); + if (typeof value === 'number') { + if (!Number.isFinite(value)) throw new Error('Canonical JSON requires finite numbers'); + return JSON.stringify(value); + } + if (typeof value === 'string') { assertUnicode(value); return JSON.stringify(value); } + if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`; + return `{${Object.keys(value).sort().map(key => { + assertUnicode(key); + return `${JSON.stringify(key)}:${canonical(value[key])}`; + }).join(',')}}`; +} + +export function cloneObject(value: unknown, label: string): ObjectValue { + return structuredClone(object(value, label)); +} + +export function selfDigest(document: ObjectValue): string { + const copy = structuredClone(document); + delete copy.contentDigest; + return sha256(canonical(copy)); +} + +export function pointerSegment(value: string): string { + return value.replaceAll('~', '~0').replaceAll('/', '~1'); +} + +export function flatten(value: Json, prefix = ''): Map { + const result = new Map(); + const walk = (item: Json, current: string): void => { + if (item && typeof item === 'object') { + if (Array.isArray(item)) item.forEach((child, index) => walk(child, `${current}/${index}`)); + else Object.keys(item).sort().forEach(key => walk(item[key], `${current}/${pointerSegment(key)}`)); + } else result.set(current || '/', canonical(item)); + }; + walk(value, prefix); + return result; +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/commands.mts b/skills/agent-team-creator/runtime/src/uar-package/commands.mts new file mode 100644 index 0000000..f557837 --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/commands.mts @@ -0,0 +1,64 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { ObjectValue, UarMigrationReceipt } from '../types.mjs'; +import { object, relativeFile, text } from '../validation.mjs'; +import { projectFile } from '../project-files.mjs'; +import { compileUarPackage } from './compiler.mjs'; +import { diffUarDirectories, diffUarPackages } from './maintenance.mjs'; +import { writeUarPackage } from './package-files.mjs'; +import { profileSchemaInfo } from './profile-validation.mjs'; +import { answerWorkspaceQuestion, initializeWorkspace, loadWorkspace, reviseWorkspace, updateWorkspaceDocument, workspaceStatus } from './workspace.mjs'; + +export const uarAuthoringCommands = [ + 'uar-workspace-init','uar-workspace-migrate','uar-workspace-status','uar-workspace-answer','uar-workspace-update','uar-workspace-revise', + 'uar-package-schema-info','uar-package-validate','uar-package-build','uar-package-diff', +] as const; + +function source(input: ObjectValue): { package: unknown; receipt?: UarMigrationReceipt } { + if (input.workspace !== undefined) { + const loaded = loadWorkspace(input); + return { package: loaded.package, ...(loaded.migrationReceipt ? { receipt: loaded.migrationReceipt } : {}) }; + } + return { package: input.package }; +} + +function outputDirectory(input: ObjectValue): string { + if (input.project === undefined) return text(input.out, 'out'); + const project = fs.realpathSync(path.resolve(text(input.project, 'project'))); + return projectFile(project, relativeFile(text(input.out, 'out'))); +} + +export function dispatchUarAuthoring(command: string, input: ObjectValue): unknown { + switch (command) { + case 'uar-workspace-init': + case 'uar-workspace-migrate': return initializeWorkspace(input); + case 'uar-workspace-status': return workspaceStatus(input); + case 'uar-workspace-answer': return answerWorkspaceQuestion(input); + case 'uar-workspace-update': return updateWorkspaceDocument(input); + case 'uar-workspace-revise': return reviseWorkspace(input); + case 'uar-package-schema-info': return profileSchemaInfo(); + case 'uar-package-validate': { + const selected = source(input); + return { valid: true, package: compileUarPackage(selected.package, selected.receipt) }; + } + case 'uar-package-build': { + const selected = source(input); + return writeUarPackage(outputDirectory(input), selected.package, selected.receipt); + } + case 'uar-package-diff': { + if (input.beforeDirectory !== undefined || input.afterDirectory !== undefined) return diffUarDirectories(input.beforeDirectory, input.afterDirectory); + if (input.beforeWorkspace !== undefined || input.afterWorkspace !== undefined) { + const project = text(input.project, 'project'); + const before = loadWorkspace({ project, workspace: text(input.beforeWorkspace, 'beforeWorkspace') }); + const after = loadWorkspace({ project, workspace: text(input.afterWorkspace, 'afterWorkspace') }); + return diffUarPackages(before.package, after.package); + } + return diffUarPackages(input.before, input.after); + } + default: throw new Error(`Unknown UAR authoring command: ${command}`); + } +} + +export function isUarAuthoringCommand(command: string): boolean { + return (uarAuthoringCommands as readonly string[]).includes(command); +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/compiler.mts b/skills/agent-team-creator/runtime/src/uar-package/compiler.mts new file mode 100644 index 0000000..4d91d97 --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/compiler.mts @@ -0,0 +1,150 @@ +import type { + Json, ObjectValue, UarAuthoringDefinition, UarCompiledPackage, + UarDefinitionKind, UarMigrationReceipt, +} from '../types.mjs'; +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { object, relativeFile, text } from '../validation.mjs'; +import { cloneObject, DIGEST, selfDigest, SEMVER, sha256 } from './canonical.mjs'; +import { validateGraph } from './graph-validation.mjs'; +import { assertMigrationAllowsBuild, normalizeAuthoring } from './migration.mjs'; +import { assertPortable } from './private-authority.mjs'; +import { validateProfileDocument } from './profile-validation.mjs'; + +const kinds = new Set(['AgentDefinition', 'TeamDefinition', 'WorkflowDefinition']); + +function identity(document: ObjectValue, label: string): { id: string; version: string; kind: UarDefinitionKind } { + if (document.profile !== UAR_PROFILE_V2) throw new Error(`${label}.profile must be ${UAR_PROFILE_V2}`); + const kind = text(document.kind, `${label}.kind`) as UarDefinitionKind; + if (!kinds.has(kind)) throw new Error(`${label}.kind is not a portable collaboration definition`); + const id = text(document.id, `${label}.id`), version = text(document.version, `${label}.version`); + if (!SEMVER.test(version)) throw new Error(`${label}.version must be semantic version x.y.z`); + return { id, version, kind }; +} + +function referenceKey(value: ObjectValue, label: string): string { + const id = text(value.id, `${label}.id`), version = text(value.version, `${label}.version`); + if (!SEMVER.test(version)) throw new Error(`${label}.version must be semantic version x.y.z`); + if (value.digest !== undefined && !DIGEST.test(text(value.digest, `${label}.digest`))) throw new Error(`${label}.digest must be sha256:<64 lowercase hex>`); + return `${id}\u0000${version}`; +} + +function replaceReferences(document: ObjectValue, resolve: (reference: ObjectValue, pointer: string) => ObjectValue): void { + if (document.kind === 'AgentDefinition') { + if (!Array.isArray(document.permittedChildren)) throw new Error('AgentDefinition.permittedChildren must be an array'); + document.permittedChildren = document.permittedChildren.map((item, index) => resolve(object(item, `permittedChildren[${index}]`), `/permittedChildren/${index}`)); + } + if (document.kind === 'TeamDefinition') { + if (!Array.isArray(document.members)) throw new Error('TeamDefinition.members must be an array'); + document.members = document.members.map((item, index) => { + const member = cloneObject(item, `members[${index}]`); + member.definition = resolve(object(member.definition, `members[${index}].definition`), `/members/${index}/definition`); + return member; + }); + const acceptance = cloneObject(document.taskAcceptance, 'taskAcceptance'); + if (!Array.isArray(acceptance.allowedWorkflows)) throw new Error('taskAcceptance.allowedWorkflows must be an array'); + acceptance.allowedWorkflows = acceptance.allowedWorkflows.map((item, index) => resolve(object(item, `taskAcceptance.allowedWorkflows[${index}]`), `/taskAcceptance/allowedWorkflows/${index}`)); + document.taskAcceptance = acceptance; + } +} + +function references(document: ObjectValue): { pointer: string; reference: ObjectValue }[] { + const result: { pointer: string; reference: ObjectValue }[] = []; + if (document.kind === 'AgentDefinition') (document.permittedChildren as Json[]).forEach((item, index) => result.push({ pointer: `/permittedChildren/${index}`, reference: object(item) })); + if (document.kind === 'TeamDefinition') { + (document.members as Json[]).forEach((item, index) => result.push({ pointer: `/members/${index}/definition`, reference: object(object(item).definition) })); + const acceptance = object(document.taskAcceptance); + (acceptance.allowedWorkflows as Json[]).forEach((item, index) => result.push({ pointer: `/taskAcceptance/allowedWorkflows/${index}`, reference: object(item) })); + } + return result; +} + +export function compileUarPackage(value: unknown, workspaceReceipt?: UarMigrationReceipt): UarCompiledPackage { + const normalized = normalizeAuthoring(value); + const receipt = workspaceReceipt ?? normalized.receipt; + assertMigrationAllowsBuild(receipt); + const manifestInput = cloneObject(normalized.package.manifest, 'package manifest'); + if (manifestInput.profile !== UAR_PROFILE_V2 || manifestInput.kind !== 'PackageManifest') throw new Error(`package manifest must use profile ${UAR_PROFILE_V2} and kind PackageManifest`); + assertPortable(manifestInput as Json, 'manifest.json'); + if (!Array.isArray(normalized.package.definitions) || normalized.package.definitions.length === 0) throw new Error('authoring package definitions must be a nonempty array'); + const definitions = normalized.package.definitions.map((source, index): UarAuthoringDefinition => { + const file = relativeFile(text(source.path, `definitions[${index}].path`)); + if (file === 'manifest.json') throw new Error('manifest.json is reserved for the compiled package manifest'); + const document = cloneObject(source.document, file); + assertPortable(document as Json, file); + return { path: file, document }; + }); + const byKey = new Map(), paths = new Set(); + for (const definition of definitions) { + const found = identity(definition.document, definition.path); + const key = `${found.id}\u0000${found.version}`; + if (byKey.has(key)) throw new Error(`Duplicate definition identity/version: ${found.id}@${found.version}`); + const folded = definition.path.normalize('NFC').toLocaleLowerCase('en-US'); + if (paths.has(folded)) throw new Error(`Case-insensitive definition path collision: ${definition.path}`); + byKey.set(key, definition); paths.add(folded); + } + + const compiled = new Map(), visiting = new Set(); + const compile = (key: string): UarAuthoringDefinition => { + const complete = compiled.get(key); if (complete) return complete; + if (visiting.has(key)) throw new Error(`Definition dependency cycle at ${key.replace('\u0000', '@')}`); + const source = byKey.get(key); if (!source) throw new Error(`Unresolved definition reference ${key.replace('\u0000', '@')}`); + visiting.add(key); + const document = cloneObject(source.document, source.path), supplied = document.contentDigest; + delete document.contentDigest; + replaceReferences(document, (reference, pointer) => { + const dependency = compile(referenceKey(reference, `${source.path}${pointer}`)); + const digest = text(dependency.document.contentDigest, `${dependency.path}.contentDigest`); + if (reference.digest !== undefined && reference.digest !== digest) throw new Error(`${source.path}${pointer}/digest conflicts with resolved definition`); + return { id: reference.id, version: reference.version, digest }; + }); + const digest = selfDigest(document); + if (supplied !== undefined && supplied !== digest) throw new Error(`Definition contentDigest conflict for ${document.id}@${document.version}; use a new version for changed content`); + document.contentDigest = digest; + validateProfileDocument(document); + if (document.kind === 'TeamDefinition' && document.instructions !== undefined) { + const instructions = object(document.instructions, `${source.path}/instructions`); + const guidance = instructions.text as string; // Shape and nonempty text were checked by the provider schema. + if (Buffer.byteLength(guidance, 'utf8') > 16_384) throw new Error(`${source.path}/instructions/text exceeds 16384 UTF-8 bytes`); + if (instructions.digest !== sha256(guidance)) throw new Error(`${source.path}/instructions/digest does not match exact UTF-8 text`); + } + const result = { path: source.path, document }; + compiled.set(key, result); visiting.delete(key); + return result; + }; + for (const key of byKey.keys()) compile(key); + + text(manifestInput.id, 'manifest.id'); + if (!SEMVER.test(text(manifestInput.version, 'manifest.version'))) throw new Error('manifest.version must be semantic version x.y.z'); + if (manifestInput.resolution !== 'exact-version-and-digest') throw new Error('manifest.resolution must be exact-version-and-digest'); + if (!Array.isArray(manifestInput.entrypoints)) throw new Error('manifest.entrypoints must be an array'); + manifestInput.entrypoints = manifestInput.entrypoints.map((entry, index) => { + const reference = object(entry, `manifest.entrypoints[${index}]`); + const definition = compile(referenceKey(reference, `manifest.entrypoints[${index}]`)); + const digest = text(definition.document.contentDigest, `${definition.path}.contentDigest`); + if (reference.digest !== undefined && reference.digest !== digest) throw new Error(`Entrypoint digest conflict for ${reference.id}@${reference.version}`); + return { id: reference.id, version: reference.version, digest }; + }); + + const files = [...compiled.values()].sort((a, b) => a.path.localeCompare(b.path)).map(definition => ({ + path: definition.path, contentUtf8: `${JSON.stringify(definition.document, null, 2)}\n`, + })); + manifestInput.files = files.map(file => { + const document = object(JSON.parse(file.contentUtf8)); + return { path: file.path, kind: document.kind, definition: { id: document.id, version: document.version, digest: document.contentDigest }, byteDigest: sha256(file.contentUtf8) }; + }); + const locks: ObjectValue[] = []; + for (const definition of compiled.values()) for (const item of references(definition.document)) { + const resolved = compiled.get(referenceKey(item.reference, `${definition.path}${item.pointer}`)); + if (!resolved) throw new Error(`Unresolved compiled reference at ${definition.path}${item.pointer}`); + locks.push({ requestedBy: definition.document.id, reference: item.reference, resolvedPath: resolved.path }); + } + manifestInput.lock = locks.sort((left, right) => String(left.requestedBy).localeCompare(String(right.requestedBy)) || String(object(left.reference).id).localeCompare(String(object(right.reference).id))); + const suppliedManifestDigest = manifestInput.contentDigest; + delete manifestInput.contentDigest; + const manifestDigest = selfDigest(manifestInput); + if (suppliedManifestDigest !== undefined && suppliedManifestDigest !== manifestDigest) throw new Error('PackageManifest contentDigest conflict; changed content requires a new version'); + manifestInput.contentDigest = manifestDigest; + validateProfileDocument(manifestInput); + validateGraph(manifestInput, [...compiled.values()]); + return { manifest: manifestInput, manifestUtf8: `${JSON.stringify(manifestInput, null, 2)}\n`, files }; +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/graph-validation.mts b/skills/agent-team-creator/runtime/src/uar-package/graph-validation.mts new file mode 100644 index 0000000..3293e8d --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/graph-validation.mts @@ -0,0 +1,147 @@ +import type { Json, ObjectValue, UarAuthoringDefinition, UarDefinitionKind } from '../types.mjs'; +import { object, text } from '../validation.mjs'; +import { DIGEST, SEMVER } from './canonical.mjs'; + +interface IndexedDefinition extends UarAuthoringDefinition { + kind: UarDefinitionKind; + key: string; +} + +const keyOf = (value: ObjectValue, label: string): string => { + const id = text(value.id, `${label}.id`); + const version = text(value.version, `${label}.version`); + if (!SEMVER.test(version)) throw new Error(`${label}.version must be semantic version x.y.z`); + if (value.digest !== undefined && !DIGEST.test(text(value.digest, `${label}.digest`))) throw new Error(`${label}.digest must be sha256:<64 lowercase hex>`); + return `${id}\u0000${version}`; +}; + +export function indexDefinitions(definitions: UarAuthoringDefinition[]): Map { + const result = new Map(); + const paths = new Set(); + for (const definition of definitions) { + const kind = String(definition.document.kind) as UarDefinitionKind; + if (!['AgentDefinition','TeamDefinition','WorkflowDefinition'].includes(kind)) throw new Error(`${definition.path}.kind is not a portable collaboration definition`); + const key = keyOf(definition.document, definition.path); + if (result.has(key)) throw new Error(`Duplicate definition identity/version: ${key.replace('\u0000', '@')}`); + const folded = definition.path.normalize('NFC').toLocaleLowerCase('en-US'); + if (paths.has(folded)) throw new Error(`Case-insensitive definition path collision: ${definition.path}`); + paths.add(folded); + result.set(key, { ...definition, kind, key }); + } + return result; +} + +function resolve(index: Map, value: unknown, expected: UarDefinitionKind, pointer: string): IndexedDefinition { + const reference = object(value, pointer); + const key = keyOf(reference, pointer); + const found = index.get(key); + if (!found) throw new Error(`${pointer} references unresolved ${key.replace('\u0000', '@')}`); + if (found.kind !== expected) throw new Error(`${pointer} expected ${expected}, observed ${found.kind}`); + const actualDigest = text(found.document.contentDigest, `${found.path}.contentDigest`); + if (reference.digest !== actualDigest) throw new Error(`${pointer}.digest does not exactly resolve ${key.replace('\u0000', '@')}`); + return found; +} + +export function validateGraph(manifest: ObjectValue, definitions: UarAuthoringDefinition[]): void { + const index = indexDefinitions(definitions); + if (!Array.isArray(manifest.entrypoints) || manifest.entrypoints.length !== 1) throw new Error('/entrypoints must contain exactly one top-level TeamDefinition'); + const root = resolve(index, manifest.entrypoints[0], 'TeamDefinition', '/entrypoints/0'); + const dependencies = new Map(); + + for (const definition of index.values()) { + const document = definition.document; + const edges: { key: string; pointer: string }[] = []; + if (definition.kind === 'AgentDefinition') { + if (!Array.isArray(document.permittedChildren)) throw new Error(`${definition.path}/permittedChildren must be an array`); + document.permittedChildren.forEach((entry, position) => { + const pointer = `${definition.path}/permittedChildren/${position}`; + edges.push({ key: resolve(index, entry, 'AgentDefinition', pointer).key, pointer }); + }); + } + if (definition.kind === 'TeamDefinition') { + if (!Array.isArray(document.members) || document.members.length === 0) throw new Error(`${definition.path}/members must be nonempty`); + const roles = new Map(); + let coordinatorAgent = false; + document.members.forEach((entry, position) => { + const member = object(entry, `${definition.path}/members/${position}`); + const role = text(member.role, `${definition.path}/members/${position}/role`); + if (roles.has(role)) throw new Error(`${definition.path}/members/${position}/role duplicates ${role}`); + const declared = member.kind === 'agent' ? 'AgentDefinition' : member.kind === 'team' ? 'TeamDefinition' : null; + if (!declared) throw new Error(`${definition.path}/members/${position}/kind must be agent or team`); + const pointer = `${definition.path}/members/${position}/definition`; + const target = resolve(index, member.definition, declared, pointer); + roles.set(role, declared); + edges.push({ key: target.key, pointer }); + if (role === document.coordinatorRole && declared === 'AgentDefinition') coordinatorAgent = true; + }); + if (!coordinatorAgent) throw new Error(`${definition.path}/coordinatorRole must name an agent member`); + if (!Array.isArray(document.communication)) throw new Error(`${definition.path}/communication must be an array`); + document.communication.forEach((entry, position) => { + const edge = object(entry, `${definition.path}/communication/${position}`); + for (const field of ['fromRole','toRole']) if (!roles.has(text(edge[field], `${definition.path}/communication/${position}/${field}`))) { + throw new Error(`${definition.path}/communication/${position}/${field} is not a team member role`); + } + }); + const acceptance = object(document.taskAcceptance, `${definition.path}/taskAcceptance`); + if (!Array.isArray(acceptance.allowedWorkflows)) throw new Error(`${definition.path}/taskAcceptance/allowedWorkflows must be an array`); + acceptance.allowedWorkflows.forEach((entry, position) => { + const pointer = `${definition.path}/taskAcceptance/allowedWorkflows/${position}`; + const workflow = resolve(index, entry, 'WorkflowDefinition', pointer); + const steps = workflow.document.steps; + if (!Array.isArray(steps)) throw new Error(`${workflow.path}/steps must be an array`); + steps.forEach((step, stepPosition) => { + const role = text(object(step, `${workflow.path}/steps/${stepPosition}`).role, `${workflow.path}/steps/${stepPosition}/role`); + if (!roles.has(role)) throw new Error(`${workflow.path}/steps/${stepPosition}/role is not accepted by team ${document.id}`); + }); + }); + const limits = object(document.limits, `${definition.path}/limits`); + if (typeof limits.maxMembers === 'number' && document.members.length > limits.maxMembers) throw new Error(`${definition.path}/members exceeds limits.maxMembers`); + } + if (definition.kind === 'WorkflowDefinition') { + if (!Array.isArray(document.steps) || document.steps.length === 0) throw new Error(`${definition.path}/steps must be nonempty`); + const steps = new Map(); + document.steps.forEach((entry, position) => { + const step = object(entry, `${definition.path}/steps/${position}`); + const id = text(step.id, `${definition.path}/steps/${position}/id`); + if (steps.has(id)) throw new Error(`${definition.path}/steps/${position}/id duplicates ${id}`); + if (!Array.isArray(step.dependsOn)) throw new Error(`${definition.path}/steps/${position}/dependsOn must be an array`); + steps.set(id, step); + }); + const stepVisiting = new Set(), stepVisited = new Set(); + const visitStep = (id: string, trail: string[]): void => { + if (stepVisiting.has(id)) throw new Error(`${definition.path} workflow dependency cycle: ${[...trail, id].join(' -> ')}`); + if (stepVisited.has(id)) return; + const step = steps.get(id); if (!step) throw new Error(`${definition.path} references unknown workflow dependency ${id}`); + stepVisiting.add(id); + for (const dependency of step.dependsOn as Json[]) visitStep(text(dependency, `${definition.path}/${id}/dependsOn`), [...trail, id]); + stepVisiting.delete(id); stepVisited.add(id); + }; + for (const id of steps.keys()) visitStep(id, []); + } + dependencies.set(definition.key, edges); + } + + const visiting = new Set(), visited = new Set(); + const visit = (key: string, trail: string[]): void => { + if (visiting.has(key)) throw new Error(`Definition dependency cycle: ${[...trail, key.replace('\u0000', '@')].join(' -> ')}`); + if (visited.has(key)) return; + visiting.add(key); + for (const edge of dependencies.get(key) ?? []) visit(edge.key, [...trail, key.replace('\u0000', '@')]); + visiting.delete(key); visited.add(key); + }; + visit(root.key, []); + for (const key of dependencies.keys()) visit(key, []); + const rootLimits = object(root.document.limits, `${root.path}/limits`); + const maxDepth = Number(rootLimits.maxDepth), maxMembers = Number(rootLimits.maxMembers); + const reachable = new Set(); + let observedDepth = 0; + const measure = (key: string, depth: number): void => { + observedDepth = Math.max(observedDepth, depth); + for (const edge of dependencies.get(key) ?? []) { + reachable.add(edge.key); measure(edge.key, depth + 1); + } + }; + measure(root.key, 0); + if (Number.isSafeInteger(maxDepth) && observedDepth > maxDepth) throw new Error(`${root.path} dependency depth ${observedDepth} exceeds limits.maxDepth ${maxDepth}`); + if (Number.isSafeInteger(maxMembers) && reachable.size > maxMembers) throw new Error(`${root.path} dependency members ${reachable.size} exceeds limits.maxMembers ${maxMembers}`); +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/maintenance.mts b/skills/agent-team-creator/runtime/src/uar-package/maintenance.mts new file mode 100644 index 0000000..747aa74 --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/maintenance.mts @@ -0,0 +1,45 @@ +import type { Json, ObjectValue, UarCompiledPackage } from '../types.mjs'; +import { object, text } from '../validation.mjs'; +import { flatten } from './canonical.mjs'; +import { compileUarPackage } from './compiler.mjs'; +import { loadUarPackage } from './package-files.mjs'; + +function identity(document: ObjectValue): string { + return `${text(document.kind, 'kind')}:${text(document.id, 'id')}`; +} + +function compare(before: UarCompiledPackage, after: UarCompiledPackage): ObjectValue { + const beforeId = text(before.manifest.id, 'before manifest id'), afterId = text(after.manifest.id, 'after manifest id'); + if (beforeId !== afterId) throw new Error('Package maintenance cannot change package identity'); + const beforeDocuments = new Map(before.files.map(file => { + const document = object(JSON.parse(file.contentUtf8), file.path); + return [identity(document), { path: file.path, document }] as const; + })); + const afterDocuments = new Map(after.files.map(file => { + const document = object(JSON.parse(file.contentUtf8), file.path); + return [identity(document), { path: file.path, document }] as const; + })); + const added = [...afterDocuments.keys()].filter(key => !beforeDocuments.has(key)).sort(); + const removed = [...beforeDocuments.keys()].filter(key => !afterDocuments.has(key)).sort(); + const changed: ObjectValue[] = []; + for (const key of [...beforeDocuments.keys()].filter(item => afterDocuments.has(item)).sort()) { + const leftDocument = beforeDocuments.get(key)!, rightDocument = afterDocuments.get(key)!; + const left = flatten(leftDocument.document as Json), right = flatten(rightDocument.document as Json); + const pointers = [...new Set([...left.keys(), ...right.keys()])].filter(pointer => left.get(pointer) !== right.get(pointer)).sort(); + if (pointers.length) changed.push({ identity: key, beforePath: leftDocument.path, afterPath: rightDocument.path, pointers }); + } + const sameVersion = before.manifest.version === after.manifest.version; + if (sameVersion && before.manifest.contentDigest !== after.manifest.contentDigest) throw new Error('Changed immutable package content requires a new semantic version'); + return { + packageId: beforeId, fromVersion: before.manifest.version, toVersion: after.manifest.version, + added, removed, changed, unchanged: before.manifest.contentDigest === after.manifest.contentDigest, + }; +} + +export function diffUarPackages(beforeValue: unknown, afterValue: unknown): ObjectValue { + return compare(compileUarPackage(beforeValue), compileUarPackage(afterValue)); +} + +export function diffUarDirectories(beforeDirectory: unknown, afterDirectory: unknown): ObjectValue { + return compare(loadUarPackage(beforeDirectory), loadUarPackage(afterDirectory)); +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/migration.mts b/skills/agent-team-creator/runtime/src/uar-package/migration.mts new file mode 100644 index 0000000..314d4a5 --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/migration.mts @@ -0,0 +1,213 @@ +import type { + Json, MigrationDiagnostic, ObjectValue, Team, UarAuthoringDefinition, + UarAuthoringPackage, UarMigrationReceipt, +} from '../types.mjs'; +import { UAR_PROFILE_V1, UAR_PROFILE_V2 } from '../types.mjs'; +import { object, text, validateTeam } from '../validation.mjs'; +import { canonical, cloneObject, pointerSegment, selfDigest, sha256 } from './canonical.mjs'; +import { assertPortable } from './private-authority.mjs'; + +export interface NormalizedAuthoring { + package: UarAuthoringPackage; + receipt: UarMigrationReceipt; +} + +const supportedCapabilities = new Set(['collaboration_definition_packages_v2']); +const contract = (): ObjectValue => ({ type: 'object', additionalProperties: true }); +const limits = (): ObjectValue => ({ concurrentTurns: 1, maxMembers: 16, maxDepth: 3, maxPendingTasks: 1000 }); +const budget = (): ObjectValue => ({ maxTokens: 100000, maxCostMicrounits: 0, currency: 'USD', maxElapsedSeconds: 3600 }); + +function safeId(value: string): string { + const cleaned = value.normalize('NFC').toLowerCase().replace(/[^a-z0-9._/-]+/g, '-').replace(/^-+|-+$/g, ''); + return cleaned || 'imported'; +} + +function diagnostic(sourceDocument: string, sourcePointer: string, disposition: MigrationDiagnostic['disposition'], reason: string, targetDocument?: string, targetPointer?: string): MigrationDiagnostic { + return { sourceDocument, sourcePointer, disposition, reason, ...(targetDocument ? { targetDocument } : {}), ...(targetPointer ? { targetPointer } : {}) }; +} + +function leafDiagnostics(value: Json, sourceDocument: string, targetDocument: string, disposition: MigrationDiagnostic['disposition']): MigrationDiagnostic[] { + const result: MigrationDiagnostic[] = []; + const walk = (item: Json, pointer: string): void => { + if (item && typeof item === 'object') { + if (Array.isArray(item)) item.forEach((child, index) => walk(child, `${pointer}/${index}`)); + else Object.entries(item).forEach(([key, child]) => walk(child, `${pointer}/${pointerSegment(key)}`)); + return; + } + result.push(diagnostic(sourceDocument, pointer || '/', disposition, disposition === 'exact' ? 'field retained without semantic translation' : 'field retained through explicit profile translation', targetDocument, pointer || '/')); + }; + walk(value, ''); + return result; +} + +function supportDiagnostics(document: ObjectValue, sourceDocument: string): MigrationDiagnostic[] { + const result: MigrationDiagnostic[] = []; + const extensions = object(document.extensions ?? {}, `${sourceDocument}.extensions`); + for (const [name, raw] of Object.entries(extensions)) { + const extension = object(raw, `${sourceDocument}.extensions.${name}`); + if (extension.required === true) result.push(diagnostic(sourceDocument, `/extensions/${pointerSegment(name)}`, 'required-unsupported', `required extension ${name} has no mini execution adapter`)); + else result.push(diagnostic(sourceDocument, `/extensions/${pointerSegment(name)}`, 'optional-unsupported', `optional extension ${name} is preserved but not executable`)); + } + if (Array.isArray(document.requiredCapabilities)) document.requiredCapabilities.forEach((capability, index) => { + if (typeof capability === 'string' && !supportedCapabilities.has(capability)) result.push(diagnostic(sourceDocument, `/requiredCapabilities/${index}`, 'required-unsupported', `required capability ${capability} is not implemented by mini`)); + }); + if (Array.isArray(document.capabilityDeclarations)) document.capabilityDeclarations.forEach((raw, index) => { + const declaration = object(raw, `${sourceDocument}.capabilityDeclarations[${index}]`); + const capability = String(declaration.capability ?? ''); + if (!supportedCapabilities.has(capability)) result.push(diagnostic(sourceDocument, `/capabilityDeclarations/${index}`, declaration.required === true ? 'required-unsupported' : 'optional-unsupported', `${declaration.required === true ? 'required' : 'optional'} capability ${capability} is not implemented by mini`)); + }); + return result; +} + +function migrateDraft1Definition(source: UarAuthoringDefinition, diagnostics: MigrationDiagnostic[]): UarAuthoringDefinition { + const document = cloneObject(source.document, source.path); + const original = structuredClone(document); + document.profile = UAR_PROFILE_V2; + delete document.contentDigest; + diagnostics.push(...leafDiagnostics(original as Json, source.path, source.path, 'exact').map(item => { + if (item.sourcePointer === '/profile') return { ...item, disposition: 'translated' as const, reason: 'draft.1 profile identifier translated to the accepted draft.2 profile' }; + if (item.sourcePointer === '/contentDigest') return { ...item, disposition: 'translated' as const, reason: 'source digest retained in source identity while draft.2 content receives a new digest' }; + return item; + })); + if (document.kind === 'AgentDefinition') { + const sourceDigest = typeof original.contentDigest === 'string' ? original.contentDigest : selfDigest(original); + document.sourceIdentity = { profile: UAR_PROFILE_V1, id: document.id, version: document.version, digest: sourceDigest, revision: null }; + document.renameMapping = { sourceId: document.id, targetId: document.id, reason: 'unchanged' }; + document.authoredFields = []; + for (const field of ['modelRequirements','promptDialect','ragConfiguration','contextStrategy','apiHarness']) document[field] = { required: false, value: {} }; + document.legacySections = document.legacySections ?? {}; + document.sourceDescriptor = document.sourceDescriptor ?? original; + if (Array.isArray(document.skills)) document.skills = document.skills.map(raw => { + const skill = cloneObject(raw, `${source.path}.skills`); + skill.entrypoint ??= null; + skill.requiredTools ??= []; + return skill; + }); + } + diagnostics.push(...supportDiagnostics(document, source.path)); + return { path: source.path, document }; +} + +function migrateInline(value: ObjectValue): NormalizedAuthoring { + const manifest = cloneObject(value.manifest, 'manifest'); + if (!Array.isArray(value.definitions)) throw new Error('authoring package definitions must be an array'); + const sourceDefinitions = value.definitions.map((entry, index) => { + const item = object(entry, `definitions[${index}]`); + return { path: text(item.path, `definitions[${index}].path`), document: cloneObject(item.document, `definitions[${index}].document`) }; + }); + assertPortable({ manifest, definitions: sourceDefinitions } as unknown as Json, 'inline-package'); + const sourceProfile = text(manifest.profile, 'manifest.profile'); + const diagnostics: MigrationDiagnostic[] = []; + let definitions: UarAuthoringDefinition[]; + if (sourceProfile === UAR_PROFILE_V1) { + definitions = sourceDefinitions.map(item => migrateDraft1Definition(item, diagnostics)); + manifest.profile = UAR_PROFILE_V2; + manifest.requiredCapabilities = ['collaboration_definition_packages_v2']; + if (Array.isArray(manifest.capabilityDeclarations)) manifest.capabilityDeclarations = [{ capability: 'collaboration_definition_packages_v2', required: true }]; + delete manifest.contentDigest; delete manifest.files; delete manifest.lock; + diagnostics.push(diagnostic('manifest.json', '/profile', 'translated', 'draft.1 manifest normalized to the accepted draft.2 profile', 'manifest.json', '/profile')); + } else if (sourceProfile === UAR_PROFILE_V2) { + definitions = sourceDefinitions; + diagnostics.push(...leafDiagnostics({ manifest, definitions } as unknown as Json, 'inline-package', 'workspace', 'exact')); + diagnostics.push(...supportDiagnostics(manifest, 'manifest.json')); + for (const item of definitions) diagnostics.push(...supportDiagnostics(item.document, item.path)); + } else throw new Error(`Unsupported collaboration source profile: ${sourceProfile}`); + const receipt: UarMigrationReceipt = { + schemaVersion: 1, sourceProfile, targetProfile: UAR_PROFILE_V2, diagnostics, + activationBlocked: diagnostics.some(item => item.disposition === 'required-unsupported'), + preservedSource: structuredClone(value) as Json, + }; + return { package: { manifest, definitions }, receipt }; +} + +function skillReference(skill: string): ObjectValue { + const id = `urn:prometheus:skill:${safeId(skill)}`; + return { id, version: '0.0.0', digest: sha256(canonical(skill)), required: false, config: { legacyName: skill }, entrypoint: null, requiredTools: [] }; +} + +function flatTeamPackage(value: unknown): NormalizedAuthoring { + const team = validateTeam(value); + assertPortable(team as unknown as Json, 'flat-team'); + const version = '1.0.0'; + const base = `urn:prometheus:agent-team:${team.id}`; + const definitions: UarAuthoringDefinition[] = team.roles.map(role => { + const id = `${base}:agent:${role.id}`; + const source = role as unknown as Json; + return { path: `agents/${role.id}.json`, document: { + profile: UAR_PROFILE_V2, kind: 'AgentDefinition', id, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, + requiredCapabilities: [], extensions: {}, title: role.id, role: role.id, + whenToUse: role.description, instructions: role.prompt, input: contract(), output: contract(), + skills: role.skills.map(skillReference), + models: [{ role: 'primary', capabilities: role.modelPolicy?.capabilities ?? [], preferredAliases: role.modelPolicy?.model ? [role.modelPolicy.model] : [] }], + permittedChildren: [], context: { mode: 'selected', artifacts: [], history: 'authorized-summary', memoryScopes: [] }, requestedLimits: limits(), + sourceIdentity: { profile: 'prometheus.agent-team/1', id: `${base}:legacy:${role.id}`, version, digest: sha256(canonical(source)), revision: null }, + renameMapping: { sourceId: `${base}:legacy:${role.id}`, targetId: id, reason: 'explicit-rename' }, authoredFields: [], + modelRequirements: { required: false, value: (role.modelPolicy ?? {}) as unknown as Json }, promptDialect: { required: false, value: {} }, + ragConfiguration: { required: false, value: {} }, contextStrategy: { required: false, value: {} }, apiHarness: { required: false, value: { id: team.harness } }, + legacySections: {}, sourceDescriptor: structuredClone(role) as unknown as Json, + } }; + }); + const workflowId = `${base}:workflow:default`; + definitions.push({ path: 'workflows/default.json', document: { + profile: UAR_PROFILE_V2, kind: 'WorkflowDefinition', id: workflowId, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, requiredCapabilities: [], extensions: {}, + title: team.outcome, input: contract(), output: contract(), steps: team.roles.map(role => ({ + id: role.id, role: role.id, dependsOn: role.dependsOn, inputMapping: {}, output: contract(), effect: 'none', approval: 'none', + retry: { maxAttempts: 1, onUnknownEffect: 'reconcile-before-retry' }, completion: 'artifact', instructions: role.prompt, + })), failurePolicy: 'stop-dependent', maxActivations: 1000, + } }); + definitions.push({ path: 'teams/root.json', document: { + profile: UAR_PROFILE_V2, kind: 'TeamDefinition', id: `${base}:team`, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, requiredCapabilities: [], extensions: {}, + title: team.id, purpose: team.outcome, members: team.roles.map(role => ({ role: role.id, kind: 'agent', definition: { id: `${base}:agent:${role.id}`, version }, min: 1, max: 1, responsibility: role.description })), + coordinatorRole: team.roles[0]!.id, communication: [], taskAcceptance: { mode: 'operator', allowedWorkflows: [{ id: workflowId, version }] }, + routing: { eligibilityFirst: true, strategy: 'operator-role-capacity-cost-stable-id', explain: true }, limits: limits(), budget: budget(), input: contract(), output: contract(), + } }); + const manifest: ObjectValue = { + profile: UAR_PROFILE_V2, kind: 'PackageManifest', id: `${base}:package`, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, + requiredCapabilities: ['collaboration_definition_packages_v2'], extensions: {}, entrypoints: [{ id: `${base}:team`, version }], + capabilityDeclarations: [{ capability: 'collaboration_definition_packages_v2', required: true }], resolution: 'exact-version-and-digest', + }; + const diagnostics = leafDiagnostics(team as unknown as Json, 'flat-team.json', 'workspace', 'translated'); + return { package: { manifest, definitions }, receipt: { + schemaVersion: 1, sourceProfile: 'prometheus.agent-team/1', targetProfile: UAR_PROFILE_V2, + diagnostics, activationBlocked: false, preservedSource: structuredClone(team) as unknown as Json, + } }; +} + +function agentArtifactPackage(value: ObjectValue): NormalizedAuthoring { + const artifactId = safeId(text(value.id, 'AgentArtifact.id')); + const metadata = object(value.metadata ?? {}, 'AgentArtifact.metadata'); + const prefer = object(object(value.policy ?? {}, 'AgentArtifact.policy').skills ?? {}, 'AgentArtifact.policy.skills').prefer; + const skills = Array.isArray(prefer) ? prefer.filter((item): item is string => typeof item === 'string') : []; + const team: Team = { + schemaVersion: 1, id: artifactId, outcome: typeof metadata.description === 'string' ? metadata.description : `Imported ${artifactId}`, + scope: 'uar', harness: 'uar', roles: [{ id: 'agent', description: typeof metadata.description === 'string' ? metadata.description : artifactId, + prompt: typeof object(value.prompt ?? {}, 'AgentArtifact.prompt').system === 'string' ? String(object(value.prompt ?? {}).system) : `Operate as ${artifactId}.`, + skills, owns: [], inputs: [], outputs: [], dependsOn: [] }], + }; + const migrated = flatTeamPackage(team); + migrated.receipt.sourceProfile = 'uar.AgentArtifact/1'; + migrated.receipt.preservedSource = structuredClone(value) as Json; + migrated.receipt.diagnostics = leafDiagnostics(value as Json, 'agent-artifact.json', 'workspace', 'translated'); + return migrated; +} + +export function normalizeAuthoring(value: unknown): NormalizedAuthoring { + const source = object(value, 'authoring input'); + if (source.package !== undefined) return normalizeAuthoring(source.package); + if (source.manifest !== undefined && source.definitions !== undefined) return migrateInline(source); + if (source.schemaVersion === 1 && Array.isArray(source.roles)) return flatTeamPackage(source); + if (source.kind === 'agent' && source.runtime !== undefined) { + assertPortable(source as Json, 'agent-artifact'); + return agentArtifactPackage(source); + } + throw new Error('Unsupported authoring input; expected workspace, inline package, flat team, or legacy AgentArtifact'); +} + +export function assertMigrationAllowsBuild(receipt: UarMigrationReceipt | undefined): void { + const blocked = receipt?.diagnostics.find(item => item.disposition === 'required-unsupported'); + if (blocked) throw new Error(`${blocked.sourceDocument}${blocked.sourcePointer}: required migration semantics are unsupported (${blocked.reason})`); +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/package-files.mts b/skills/agent-team-creator/runtime/src/uar-package/package-files.mts new file mode 100644 index 0000000..2a9c33e --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/package-files.mts @@ -0,0 +1,57 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { randomUUID } from 'node:crypto'; +import type { ObjectValue, UarAuthoringPackage, UarCompiledPackage, UarMigrationReceipt } from '../types.mjs'; +import { object, relativeFile, text } from '../validation.mjs'; +import { selfDigest, sha256 } from './canonical.mjs'; +import { compileUarPackage } from './compiler.mjs'; + +function writeExclusive(file: string, content: string): void { + fs.mkdirSync(path.dirname(file), { recursive: true }); + const descriptor = fs.openSync(file, 'wx', 0o600); + try { fs.writeFileSync(descriptor, content); fs.fsyncSync(descriptor); } + finally { fs.closeSync(descriptor); } +} + +export function writeUarPackage(directoryValue: unknown, value: unknown, receipt?: UarMigrationReceipt): { directory: string; manifest: ObjectValue; files: string[] } { + const compiled = compileUarPackage(value, receipt); + const directory = path.resolve(text(directoryValue, 'out')); + if (fs.existsSync(directory)) throw new Error(`Immutable package destination already exists: ${directory}`); + fs.mkdirSync(path.dirname(directory), { recursive: true }); + const temporary = path.join(path.dirname(directory), `.${path.basename(directory)}.${randomUUID()}.tmp`); + fs.mkdirSync(temporary); + try { + for (const file of compiled.files) writeExclusive(path.join(temporary, ...file.path.split('/')), file.contentUtf8); + writeExclusive(path.join(temporary, 'manifest.json'), compiled.manifestUtf8); + fs.renameSync(temporary, directory); + } finally { fs.rmSync(temporary, { recursive: true, force: true }); } + return { directory, manifest: compiled.manifest, files: ['manifest.json', ...compiled.files.map(file => file.path)] }; +} + +export function loadUarPackage(directoryValue: unknown): UarCompiledPackage { + const directory = path.resolve(text(directoryValue, 'packageDirectory')); + const manifestUtf8 = fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'); + const manifest = object(JSON.parse(manifestUtf8), 'manifest'); + if (!Array.isArray(manifest.files)) throw new Error('Compiled package manifest.files must be an array'); + const files = manifest.files.map((entry, index) => { + const item = object(entry, `manifest.files[${index}]`); + const file = relativeFile(text(item.path, `manifest.files[${index}].path`)); + const contentUtf8 = fs.readFileSync(path.join(directory, ...file.split('/')), 'utf8'); + if (sha256(contentUtf8) !== item.byteDigest) throw new Error(`Package byte digest mismatch: ${file}`); + const document = object(JSON.parse(contentUtf8), file); + if (selfDigest(document) !== document.contentDigest) throw new Error(`Package content digest mismatch: ${file}`); + const reference = object(item.definition, `manifest.files[${index}].definition`); + for (const field of ['id','version']) if (document[field] !== reference[field]) throw new Error(`Package file identity mismatch: ${file} ${field}`); + if (document.contentDigest !== reference.digest) throw new Error(`Package file identity mismatch: ${file} digest`); + if (document.kind !== item.kind) throw new Error(`Package file kind mismatch: ${file}`); + return { path: file, contentUtf8 }; + }); + if (selfDigest(manifest) !== manifest.contentDigest) throw new Error('Package manifest content digest mismatch'); + return { manifest, manifestUtf8, files }; +} + +export function compiledAsAuthoring(compiled: UarCompiledPackage): UarAuthoringPackage { + const manifest = structuredClone(compiled.manifest); + delete manifest.files; delete manifest.lock; + return { manifest, definitions: compiled.files.map(file => ({ path: file.path, document: object(JSON.parse(file.contentUtf8), file.path) })) }; +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/private-authority.mts b/skills/agent-team-creator/runtime/src/uar-package/private-authority.mts new file mode 100644 index 0000000..1715c45 --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/private-authority.mts @@ -0,0 +1,65 @@ +import type { Json, MigrationDiagnostic } from '../types.mjs'; +import { pointerSegment } from './canonical.mjs'; + +const privateKinds = new Set(['DeploymentBinding', 'RepresentationGrant', 'EffectiveBindingReceipt']); +const forbiddenKeys = /^(?:credential|credentialValue|token|secret|password|apiKey|privateKey|consentEvidence(?:Ref)?|representationGrant(?:Ref|Refs)?|effectiveBindingReceiptRef|installedAuthority)$/i; +const recognizedSecret = /(?:^|\b)(?:token|secret|password|api[-_ ]?key)\s*(?:=|:|is)?\s*[A-Za-z0-9_./+:-]{16,}/i; + +export function privateAuthorityDiagnostics(value: Json, sourceDocument: string): MigrationDiagnostic[] { + const diagnostics: MigrationDiagnostic[] = []; + const walk = (item: Json, pointer: string): void => { + if (typeof item === 'string' && recognizedSecret.test(item)) diagnostics.push({ + sourceDocument, sourcePointer: pointer || '/', disposition: 'required-unsupported', + reason: 'recognized credential or secret material is forbidden in portable content', + }); + if (Array.isArray(item)) return item.forEach((child, index) => walk(child, `${pointer}/${index}`)); + if (!item || typeof item !== 'object') return; + if (typeof item.kind === 'string' && privateKinds.has(item.kind)) diagnostics.push({ + sourceDocument, sourcePointer: `${pointer}/kind`, disposition: 'required-unsupported', + reason: `${item.kind} is private installed state and cannot enter a portable package`, + }); + for (const [key, child] of Object.entries(item)) { + const childPointer = `${pointer}/${pointerSegment(key)}`; + if (forbiddenKeys.test(key)) diagnostics.push({ + sourceDocument, sourcePointer: childPointer, disposition: 'required-unsupported', + reason: 'private credential, consent, or authority values are forbidden in portable content', + }); + else if (key === 'credentialRef' && sourceDocument !== 'deployment-binding') diagnostics.push({ + sourceDocument, sourcePointer: childPointer, disposition: 'required-unsupported', + reason: 'credential references belong only in a private deployment binding', + }); + walk(child, childPointer); + } + }; + walk(value, ''); + return diagnostics; +} + +export function assertPortable(value: Json, sourceDocument: string): void { + const diagnostics = privateAuthorityDiagnostics(value, sourceDocument); + if (diagnostics.length) throw new Error(`${diagnostics[0]!.sourceDocument}${diagnostics[0]!.sourcePointer}: ${diagnostics[0]!.reason}`); +} + +export function assertOpaqueReference(value: unknown, pointer: string): string { + if (typeof value !== 'string' || !value.trim()) throw new Error(`${pointer} must be a nonempty opaque reference`); + if (/^(?:bearer|token|secret|password|api[-_]?key):/i.test(value) || recognizedSecret.test(value)) { + throw new Error(`${pointer} must be an opaque host reference, not a credential value`); + } + return value; +} + +export function assertBindingContainsReferencesOnly(value: Json): void { + const walk = (item: Json, pointer: string): void => { + if (typeof item === 'string' && recognizedSecret.test(item)) throw new Error(`${pointer || '/'} contains recognized credential material`); + if (Array.isArray(item)) return item.forEach((child, index) => walk(child, `${pointer}/${index}`)); + if (!item || typeof item !== 'object') return; + for (const [key, child] of Object.entries(item)) { + const childPointer = `${pointer}/${pointerSegment(key)}`; + if (/^(?:credential|credentialValue|token|secret|password|apiKey|privateKey|consentEvidence|installedAuthority)$/i.test(key)) { + throw new Error(`${childPointer} contains a private value; use an opaque reference field`); + } + walk(child, childPointer); + } + }; + walk(value, ''); +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/profile-validation.mts b/skills/agent-team-creator/runtime/src/uar-package/profile-validation.mts new file mode 100644 index 0000000..892ccc3 --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/profile-validation.mts @@ -0,0 +1,132 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import type { Json, ObjectValue } from '../types.mjs'; +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { canonical, pointerSegment } from './canonical.mjs'; + +type Schema = { [key: string]: unknown }; +const schemaDirectory = fileURLToPath(new URL('../../schemas/uar/0.1.0-draft.2/', import.meta.url)); +const schemaFiles: Record = { + AgentDefinition: 'agent-definition.schema.json', + TeamDefinition: 'team-definition.schema.json', + WorkflowDefinition: 'workflow-definition.schema.json', + PackageManifest: 'package-manifest.schema.json', + DeploymentBinding: 'deployment-binding.schema.json', + ConversionReport: 'conversion-report.schema.json', +}; +const cache = new Map(); + +function schema(file: string): Schema { + const found = cache.get(file); + if (found) return found; + const value = JSON.parse(readFileSync(new URL(`../../schemas/uar/0.1.0-draft.2/${file}`, import.meta.url), 'utf8')) as Schema; + cache.set(file, value); + return value; +} + +function resolvePointer(root: Schema, fragment: string): Schema { + let current: unknown = root; + if (!fragment || fragment === '#') return root; + if (!fragment.startsWith('#/')) throw new Error(`Unsupported schema reference fragment: ${fragment}`); + for (const part of fragment.slice(2).split('/')) { + const key = part.replaceAll('~1', '/').replaceAll('~0', '~'); + if (!current || typeof current !== 'object' || !(key in current)) throw new Error(`Unresolved schema reference: ${fragment}`); + current = (current as Record)[key]; + } + if (!current || typeof current !== 'object' || Array.isArray(current)) throw new Error(`Schema reference is not an object: ${fragment}`); + return current as Schema; +} + +function resolveReference(reference: string, currentFile: string): { file: string; value: Schema } { + const [filePart, fragmentPart] = reference.split('#', 2); + const file = filePart || currentFile; + return { file, value: resolvePointer(schema(file), fragmentPart === undefined ? '#' : `#${fragmentPart}`) }; +} + +function typeMatches(value: unknown, type: string): boolean { + if (type === 'null') return value === null; + if (type === 'array') return Array.isArray(value); + if (type === 'object') return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + if (type === 'integer') return Number.isSafeInteger(value); + if (type === 'number') return typeof value === 'number' && Number.isFinite(value); + return typeof value === type; +} + +function failure(pointer: string, message: string): never { + throw new Error(`Schema validation failed at ${pointer || '/'}: ${message}`); +} + +function validateNode(value: unknown, rule: Schema, file: string, pointer: string): void { + if (typeof rule.$ref === 'string') { + const resolved = resolveReference(rule.$ref, file); + validateNode(value, resolved.value, resolved.file, pointer); + return; + } + if (Array.isArray(rule.oneOf)) { + const matches = rule.oneOf.filter(candidate => { + try { validateNode(value, candidate as Schema, file, pointer); return true; } + catch { return false; } + }); + if (matches.length !== 1) failure(pointer, `expected exactly one schema branch, observed ${matches.length}`); + return; + } + if ('const' in rule && canonical(value as Json) !== canonical(rule.const as Json)) failure(pointer, `must equal ${JSON.stringify(rule.const)}`); + if (Array.isArray(rule.enum) && !rule.enum.some(item => canonical(item as Json) === canonical(value as Json))) failure(pointer, 'value is outside the allowed enumeration'); + if (rule.type !== undefined) { + const types = Array.isArray(rule.type) ? rule.type : [rule.type]; + if (!types.some(type => typeof type === 'string' && typeMatches(value, type))) failure(pointer, `expected type ${types.join(' or ')}`); + } + if (typeof value === 'string') { + if (typeof rule.minLength === 'number' && value.length < rule.minLength) failure(pointer, `must contain at least ${rule.minLength} characters`); + if (typeof rule.maxLength === 'number' && [...value].length > rule.maxLength) failure(pointer, `must contain at most ${rule.maxLength} characters`); + if (typeof rule.pattern === 'string' && !new RegExp(rule.pattern, 'u').test(value)) failure(pointer, `does not match ${rule.pattern}`); + if (rule.format === 'uri') { try { new URL(value); } catch { failure(pointer, 'must be a URI'); } } + if (rule.format === 'uri-reference') { try { new URL(value, 'https://schemas.prometheus-ags.dev/'); } catch { failure(pointer, 'must be a URI reference'); } } + if (rule.format === 'date-time' && !Number.isFinite(Date.parse(value))) failure(pointer, 'must be an RFC 3339 date-time'); + } + if (typeof value === 'number') { + if (typeof rule.minimum === 'number' && value < rule.minimum) failure(pointer, `must be at least ${rule.minimum}`); + if (typeof rule.maximum === 'number' && value > rule.maximum) failure(pointer, `must be at most ${rule.maximum}`); + } + if (Array.isArray(value)) { + if (typeof rule.minItems === 'number' && value.length < rule.minItems) failure(pointer, `must contain at least ${rule.minItems} items`); + if (typeof rule.maxItems === 'number' && value.length > rule.maxItems) failure(pointer, `must contain at most ${rule.maxItems} items`); + if (rule.uniqueItems === true) { + const keys = value.map(item => canonical(item as Json)); + if (new Set(keys).size !== keys.length) failure(pointer, 'items must be unique'); + } + if (rule.items && typeof rule.items === 'object') value.forEach((item, index) => validateNode(item, rule.items as Schema, file, `${pointer}/${index}`)); + } + if (value && typeof value === 'object' && !Array.isArray(value)) { + const record = value as Record; + const properties = rule.properties && typeof rule.properties === 'object' ? rule.properties as Record : {}; + if (Array.isArray(rule.required)) for (const key of rule.required) { + if (typeof key === 'string' && !(key in record)) failure(`${pointer}/${pointerSegment(key)}`, 'required property is missing'); + } + for (const [key, child] of Object.entries(record)) { + const childPointer = `${pointer}/${pointerSegment(key)}`; + if (properties[key]) validateNode(child, properties[key], file, childPointer); + else if (rule.additionalProperties === false) failure(childPointer, 'additional property is forbidden'); + else if (rule.additionalProperties && typeof rule.additionalProperties === 'object') validateNode(child, rule.additionalProperties as Schema, file, childPointer); + if (rule.propertyNames && typeof rule.propertyNames === 'object') validateNode(key, rule.propertyNames as Schema, file, childPointer); + } + } +} + +export function validateProfileDocument(value: ObjectValue): void { + if (value.profile !== UAR_PROFILE_V2) throw new Error(`profile must be ${UAR_PROFILE_V2}`); + const kind = String(value.kind ?? ''); + const file = schemaFiles[kind]; + if (!file) throw new Error(`Unsupported draft.2 document kind: ${kind}`); + validateNode(value, schema(file), file, ''); +} + +export function profileSchemaInfo(): ObjectValue { + const receipt = JSON.parse(readFileSync(new URL('../../schemas/uar/0.1.0-draft.2/consumer-source-receipt.json', import.meta.url), 'utf8')) as { provider: { commit: string } }; + return { + profile: UAR_PROFILE_V2, + sourceRevision: receipt.provider.commit, + directory: schemaDirectory, + documents: structuredClone(schemaFiles), + }; +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/workspace-questions.mts b/skills/agent-team-creator/runtime/src/uar-package/workspace-questions.mts new file mode 100644 index 0000000..b0f162b --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/workspace-questions.mts @@ -0,0 +1,100 @@ +import type { + Json, ObjectValue, UarAuthoringDefinition, UarQuestionNode, UarQuestionState, +} from '../types.mjs'; +import { object } from '../validation.mjs'; + +type QuestionSeed = Omit; + +function decode(segment: string): string { + return segment.replaceAll('~1', '/').replaceAll('~0', '~'); +} + +export function pointerValue(document: Json | undefined, pointer: string): Json | undefined { + if (pointer === '') return document; + let current: Json | undefined = document; + for (const raw of pointer.split('/').slice(1)) { + if (current === null || typeof current !== 'object') return undefined; + const key = decode(raw); + current = Array.isArray(current) ? current[Number(key)] : current[key]; + } + return current; +} + +export function setPointer(document: ObjectValue, pointer: string, value: Json): void { + const segments = pointer.split('/').slice(1).map(decode); + if (!segments.length) throw new Error('A guided answer cannot replace a complete document'); + let current: ObjectValue = document; + for (const segment of segments.slice(0, -1)) { + const existing = current[segment]; + if (existing === undefined) current[segment] = {}; + current = object(current[segment], `question target ${pointer}`); + } + current[segments.at(-1)!] = structuredClone(value); +} + +function seed(id: string, document: string, pointer: string, prompt: string): QuestionSeed { + return { id, document, pointer, prompt, required: true }; +} + +function seeds(manifestPath: string, definitions: UarAuthoringDefinition[]): QuestionSeed[] { + const result = [ + seed('manifest.provenance', manifestPath, '/provenance', 'What source and authors establish package provenance?'), + seed('manifest.entrypoint', manifestPath, '/entrypoints', 'Which single TeamDefinition is the top-level package entrypoint?'), + ]; + const teams = definitions.filter(item => item.document.kind === 'TeamDefinition'); + const agents = definitions.filter(item => item.document.kind === 'AgentDefinition'); + const workflows = definitions.filter(item => item.document.kind === 'WorkflowDefinition'); + for (const definition of teams) { + const prefix = `team.${definition.document.id}`; + result.push( + seed(`${prefix}.members`, definition.path, '/members', `Which agents or nested teams belong to ${definition.document.id}?`), + seed(`${prefix}.coordinator`, definition.path, '/coordinatorRole', `Which agent role coordinates ${definition.document.id}?`), + seed(`${prefix}.communication`, definition.path, '/communication', `Which explicit role communication edges are allowed for ${definition.document.id}?`), + seed(`${prefix}.acceptance`, definition.path, '/taskAcceptance/allowedWorkflows', `Which exact workflows may ${definition.document.id} accept?`), + seed(`${prefix}.limits`, definition.path, '/limits', `Which aggregate limits constrain ${definition.document.id}?`), + seed(`${prefix}.budget`, definition.path, '/budget', `Which aggregate budget constrains ${definition.document.id}?`), + ); + } + for (const definition of agents) { + const prefix = `agent.${definition.document.id}`; + result.push( + seed(`${prefix}.children`, definition.path, '/permittedChildren', `Which exact child agents may ${definition.document.id} invoke?`), + seed(`${prefix}.skills`, definition.path, '/skills', `Which exact skill locks does ${definition.document.id} require?`), + seed(`${prefix}.models`, definition.path, '/models', `Which model capabilities and aliases does ${definition.document.id} request?`), + seed(`${prefix}.context`, definition.path, '/context', `Which bounded context may ${definition.document.id} receive?`), + seed(`${prefix}.limits`, definition.path, '/requestedLimits', `Which limits constrain ${definition.document.id}?`), + ); + } + for (const definition of workflows) result.push( + seed(`workflow.${definition.document.id}.steps`, definition.path, '/steps', `Which finite role steps define ${definition.document.id}?`), + ); + result.push(seed('workspace.binding-intent', 'workspace.json', '/bindingIntent', 'Should deployment stop after package installation or prepare a private binding?')); + return result; +} + +function documentMap(manifestPath: string, manifest: ObjectValue, definitions: UarAuthoringDefinition[], bindingIntent?: string): Map { + return new Map([ + [manifestPath, manifest], + ...definitions.map(item => [item.path, item.document] as [string, Json]), + ['workspace.json', { bindingIntent: bindingIntent ?? null }], + ]); +} + +export function questionState( + manifestPath: string, + manifest: ObjectValue, + definitions: UarAuthoringDefinition[], + bindingIntent?: string, + previous?: UarQuestionState, +): UarQuestionState { + const documents = documentMap(manifestPath, manifest, definitions, bindingIntent); + const prior = new Map(previous?.questions.map(question => [question.id, question]) ?? []); + const questions = seeds(manifestPath, definitions).map((item): UarQuestionNode => { + const value = pointerValue(documents.get(item.document), item.pointer); + const accepted = prior.get(item.id)?.answer; + const answer = value === undefined || value === null ? accepted : value; + return { ...item, state: answer === undefined || answer === null ? 'pending' : 'answered', ...(answer === undefined || answer === null ? {} : { answer: structuredClone(answer) }) }; + }); + const currentQuestionId = questions.find(item => item.required && item.state === 'pending')?.id ?? null; + return { currentQuestionId, questions }; +} diff --git a/skills/agent-team-creator/runtime/src/uar-package/workspace.mts b/skills/agent-team-creator/runtime/src/uar-package/workspace.mts new file mode 100644 index 0000000..c64112e --- /dev/null +++ b/skills/agent-team-creator/runtime/src/uar-package/workspace.mts @@ -0,0 +1,285 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { randomUUID } from 'node:crypto'; +import type { + Json, ObjectValue, UarAuthoringDefinition, UarMigrationReceipt, UarQuestionState, + UarWorkspace, UarWorkspaceIndex, UarWorkspaceStatus, +} from '../types.mjs'; +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { id, object, relativeFile, text } from '../validation.mjs'; +import { commitChanges, projectFile, readFile, stage, type Change } from '../project-files.mjs'; +import { SEMVER } from './canonical.mjs'; +import { compileUarPackage } from './compiler.mjs'; +import { normalizeAuthoring } from './migration.mjs'; +import { compiledAsAuthoring, loadUarPackage } from './package-files.mjs'; +import { questionState, setPointer } from './workspace-questions.mjs'; + +const INDEX_FILE = 'workspace.json'; +const DEFAULT_PAGE_SIZE = 20; +const MAX_PAGE_SIZE = 50; + +function projectRoot(input: ObjectValue): string { + return fs.realpathSync(path.resolve(text(input.project, 'project'))); +} + +function teamId(input: ObjectValue, field = 'workspace'): string { + return id(input[field], field); +} + +function workspacePath(input: ObjectValue, field = 'workspace'): string { + return `.agent-team/${teamId(input, field)}/authoring`; +} + +function expectedRevision(input: ObjectValue): number { + if (!Number.isSafeInteger(input.expectedRevision) || Number(input.expectedRevision) < 0) throw new Error('expectedRevision must be a nonnegative integer from the current workspace'); + return Number(input.expectedRevision); +} + +function json(value: unknown): string { + return `${JSON.stringify(value, null, 2)}\n`; +} + +function readJson(file: string, label: string): ObjectValue { + return object(JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')), label); +} + +function validateQuestionState(value: unknown): UarQuestionState { + const state = object(value, 'workspace.questionState'); + if (!Array.isArray(state.questions)) throw new Error('workspace.questionState.questions must be an array'); + const ids = new Set(); + for (const raw of state.questions) { + const question = object(raw, 'workspace question'), questionId = text(question.id, 'workspace question id'); + if (ids.has(questionId)) throw new Error(`Duplicate workspace question id: ${questionId}`); + ids.add(questionId); relativeFile(text(question.document, 'workspace question document')); + if (typeof question.pointer !== 'string' || !question.pointer.startsWith('/')) throw new Error('workspace question pointer must be a JSON Pointer'); + text(question.prompt, 'workspace question prompt'); + if (question.required !== true) throw new Error('workspace questions must be mandatory'); + if (!['pending','answered'].includes(String(question.state))) throw new Error('workspace question state must be pending or answered'); + if (question.state === 'answered' && question.answer === undefined) throw new Error(`Answered workspace question lacks an answer: ${questionId}`); + } + if (state.currentQuestionId !== null && (typeof state.currentQuestionId !== 'string' || !ids.has(state.currentQuestionId))) throw new Error('workspace.currentQuestionId must name a persisted question or be null'); + return structuredClone(state) as unknown as UarQuestionState; +} + +function validateIndex(value: unknown): UarWorkspaceIndex { + const index = object(value, 'workspace index'); + const allowed = new Set(['schemaVersion','revision','profile','teamId','packageId','packageVersion','manifest','definitions','migrationReceipt','bindingIntent','base','questionState']); + for (const field of Object.keys(index)) if (!allowed.has(field)) throw new Error(`Unknown workspace index field: ${field}`); + if (index.schemaVersion !== 1) throw new Error('workspace.schemaVersion must be 1'); + if (!Number.isSafeInteger(index.revision) || Number(index.revision) < 1) throw new Error('workspace.revision must be a positive integer'); + if (index.profile !== UAR_PROFILE_V2) throw new Error(`workspace.profile must be ${UAR_PROFILE_V2}`); + id(index.teamId, 'workspace.teamId'); text(index.packageId, 'workspace.packageId'); + if (!SEMVER.test(text(index.packageVersion, 'workspace.packageVersion'))) throw new Error('workspace.packageVersion must be semantic version x.y.z'); + relativeFile(text(index.manifest, 'workspace.manifest')); + if (!Array.isArray(index.definitions) || index.definitions.length === 0) throw new Error('workspace.definitions must be a nonempty array'); + const seen = new Set(); + for (const [position, item] of index.definitions.entries()) { + const file = relativeFile(text(item, `workspace.definitions[${position}]`)), folded = file.normalize('NFC').toLocaleLowerCase('en-US'); + if (seen.has(folded)) throw new Error(`Case-insensitive workspace path collision: ${file}`); + seen.add(folded); + } + const manifestFolded = String(index.manifest).normalize('NFC').toLocaleLowerCase('en-US'); + if (seen.has(manifestFolded) || manifestFolded === INDEX_FILE) throw new Error('Workspace manifest path collides with another source document'); + if (index.migrationReceipt !== undefined) relativeFile(text(index.migrationReceipt, 'workspace.migrationReceipt')); + if (index.bindingIntent !== undefined && !['package-only','package-and-binding'].includes(String(index.bindingIntent))) throw new Error('Invalid workspace.bindingIntent'); + if (index.base !== undefined) { + const base = object(index.base, 'workspace.base'); id(base.teamId, 'workspace.base.teamId'); + if (!Number.isSafeInteger(base.revision) || Number(base.revision) < 1) throw new Error('workspace.base.revision must be positive'); + if (!SEMVER.test(text(base.packageVersion, 'workspace.base.packageVersion'))) throw new Error('workspace.base.packageVersion must be semantic'); + } + validateQuestionState(index.questionState); + return structuredClone(index) as unknown as UarWorkspaceIndex; +} + +function lock(project: string, workspace: string): () => void { + const lockFile = projectFile(project, `${workspace}.lock`); fs.mkdirSync(path.dirname(lockFile), { recursive: true }); + const token = randomUUID(); let descriptor: number; + try { descriptor = fs.openSync(lockFile, 'wx', 0o600); } + catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EEXIST') throw new Error(`Workspace lock held: ${workspace}.lock; inspect it before manual recovery`); + throw error; + } + try { fs.writeFileSync(descriptor, JSON.stringify({ token, pid: process.pid, at: new Date().toISOString() })); fs.fsyncSync(descriptor); } + catch (error) { fs.closeSync(descriptor); fs.rmSync(lockFile, { force: true }); throw error; } + fs.closeSync(descriptor); + return () => { try { if (JSON.parse(fs.readFileSync(lockFile, 'utf8')).token === token) fs.rmSync(lockFile); } catch { /* replaced lock belongs to another writer */ } }; +} + +export function loadWorkspace(input: ObjectValue): UarWorkspace { + const project = projectRoot(input), workspace = workspacePath(input), base = `${workspace}/${INDEX_FILE}`; + const index = validateIndex(readJson(projectFile(project, base), base)); + if (index.teamId !== teamId(input)) throw new Error('Workspace team identity does not match its project path'); + const manifestPath = `${workspace}/${index.manifest}`, manifest = readJson(projectFile(project, manifestPath), manifestPath); + if (manifest.id !== index.packageId || manifest.version !== index.packageVersion) throw new Error('Workspace index package identity/version does not match its manifest source'); + const definitions: UarAuthoringDefinition[] = index.definitions.map(file => ({ path: file, document: readJson(projectFile(project, `${workspace}/${file}`), `${workspace}/${file}`) })); + let migrationReceipt: UarMigrationReceipt | undefined; + if (index.migrationReceipt) migrationReceipt = readJson(projectFile(project, `${workspace}/${index.migrationReceipt}`), 'migration receipt') as unknown as UarMigrationReceipt; + return { root: workspace, index, package: { manifest, definitions }, ...(migrationReceipt ? { migrationReceipt } : {}) }; +} + +function assertRevision(workspace: UarWorkspace, expected: number): void { + if (workspace.index.revision !== expected) throw new Error(`Stale workspace revision: expected ${expected}, current ${workspace.index.revision}; reload before writing`); +} + +export function initializeWorkspace(input: ObjectValue): ObjectValue { + const project = projectRoot(input), workspaceId = teamId(input), workspace = workspacePath(input); + if (expectedRevision(input) !== 0) throw new Error('New workspace expectedRevision must be 0'); + const release = lock(project, workspace); + try { + const indexFile = projectFile(project, `${workspace}/${INDEX_FILE}`); + if (fs.existsSync(indexFile)) throw new Error(`Workspace already exists: ${workspaceId}`); + const migrationSource = input.sourceDirectory === undefined ? input.source : compiledAsAuthoring(loadUarPackage(projectFile(project, relativeFile(text(input.sourceDirectory, 'sourceDirectory'))))); + const normalized = migrationSource === undefined ? null : normalizeAuthoring(migrationSource); + const packageId = normalized ? text(normalized.package.manifest.id, 'manifest.id') : text(input.packageId, 'packageId'); + const packageVersion = normalized ? text(normalized.package.manifest.version, 'manifest.version') : text(input.packageVersion, 'packageVersion'); + if (!SEMVER.test(packageVersion)) throw new Error('packageVersion must be semantic version x.y.z'); + const definitionPaths = normalized ? normalized.package.definitions.map(item => item.path) : (() => { + if (!Array.isArray(input.definitionPaths) || input.definitionPaths.length === 0) throw new Error('definitionPaths must declare at least one source document path'); + return input.definitionPaths.map((item, position) => relativeFile(text(item, `definitionPaths[${position}]`))); + })(); + const manifestPath = input.manifestPath === undefined ? 'manifest.source.json' : relativeFile(text(input.manifestPath, 'manifestPath')); + const manifest: ObjectValue = normalized ? normalized.package.manifest : { profile: UAR_PROFILE_V2, kind: 'PackageManifest', id: packageId, version: packageVersion }; + const definitions = normalized ? normalized.package.definitions : definitionPaths.map(file => ({ path: file, document: {} as ObjectValue })); + const migrationReceipt: UarMigrationReceipt = normalized?.receipt ?? { schemaVersion: 1, sourceProfile: 'new-authoring', targetProfile: UAR_PROFILE_V2, diagnostics: [], activationBlocked: false }; + const bindingIntent = input.bindingIntent ?? 'package-only'; + const index = validateIndex({ schemaVersion: 1, revision: 1, profile: UAR_PROFILE_V2, teamId: workspaceId, packageId, packageVersion, manifest: manifestPath, definitions: definitionPaths, migrationReceipt: 'migration-receipt.json', bindingIntent, questionState: questionState(manifestPath, manifest, definitions, String(bindingIntent)) }); + const changes = new Map(); stage(changes, project, `${workspace}/${INDEX_FILE}`, json(index)); stage(changes, project, `${workspace}/${index.manifest}`, json(manifest)); + for (const definition of definitions) stage(changes, project, `${workspace}/${definition.path}`, json(definition.document)); + stage(changes, project, `${workspace}/${index.migrationReceipt}`, json(migrationReceipt)); + const recovery = commitChanges(project, [...changes.values()], { operation: 'workspace-init', teamId: workspaceId, beforeRevision: 0, afterRevision: 1 }); + return { workspace: workspaceId, path: workspace, revision: 1, packageId: index.packageId, packageVersion: index.packageVersion, documents: definitions.length + 2, recovery }; + } finally { release(); } +} + +function stageMutation(project: string, workspace: UarWorkspace, operation: string, changes: Map): ObjectValue { + const beforeRevision = workspace.index.revision, afterRevision = beforeRevision + 1; workspace.index.revision = afterRevision; + stage(changes, project, `${workspace.root}/${INDEX_FILE}`, json(workspace.index)); + const recovery = commitChanges(project, [...changes.values()], { operation, teamId: workspace.index.teamId, beforeRevision, afterRevision }); + return { workspace: workspace.index.teamId, path: workspace.root, beforeRevision, revision: afterRevision, recovery }; +} + +export function updateWorkspaceDocument(input: ObjectValue): ObjectValue { + const project = projectRoot(input), workspace = workspacePath(input), file = relativeFile(text(input.path, 'path')), release = lock(project, workspace); + try { + const current = loadWorkspace(input); assertRevision(current, expectedRevision(input)); + const allowed = new Set([current.index.manifest, ...current.index.definitions]); if (!allowed.has(file)) throw new Error(`Workspace update path is not declared by workspace.json: ${file}`); + const document = object(input.document, 'document'); + if (file === current.index.manifest) { + if (document.kind !== 'PackageManifest' || document.id !== current.index.packageId || document.version !== current.index.packageVersion) throw new Error('Manifest update cannot change package kind, identity, or version'); + current.package.manifest = structuredClone(document); + } else { + if (!['AgentDefinition','TeamDefinition','WorkflowDefinition'].includes(String(document.kind))) throw new Error('Definition update must contain one portable collaboration definition'); + const selected = current.package.definitions.find(item => item.path === file)!; + if (selected.document.kind !== document.kind || selected.document.id !== document.id || selected.document.version !== document.version) throw new Error('Document update cannot change immutable kind, identity, or version; use workspace revision'); + selected.document = structuredClone(document); + } + current.index.questionState = questionState(current.index.manifest, current.package.manifest, current.package.definitions, current.index.bindingIntent, current.index.questionState); + const changes = new Map(); stage(changes, project, `${workspace}/${file}`, json(document)); + return { ...stageMutation(project, current, 'workspace-update', changes), path: file, kind: document.kind, id: document.id, version: document.version }; + } finally { release(); } +} + +export function answerWorkspaceQuestion(input: ObjectValue): ObjectValue { + const project = projectRoot(input), workspace = workspacePath(input), release = lock(project, workspace); + try { + const current = loadWorkspace(input); assertRevision(current, expectedRevision(input)); + const questionId = text(input.questionId, 'questionId'), question = current.index.questionState.questions.find(item => item.id === questionId); + if (!question) throw new Error(`Unknown workspace question: ${questionId}`); if (input.answer === undefined) throw new Error('answer is required'); + const changes = new Map(); + if (question.document === INDEX_FILE) { + if (question.pointer !== '/bindingIntent' || !['package-only','package-and-binding'].includes(String(input.answer))) throw new Error('Invalid binding-intent answer'); + current.index.bindingIntent = input.answer as 'package-only' | 'package-and-binding'; + } else { + const selected = question.document === current.index.manifest ? current.package.manifest : current.package.definitions.find(item => item.path === question.document)?.document; + if (!selected) throw new Error(`Question document is no longer declared: ${question.document}`); + setPointer(selected, question.pointer, input.answer as Json); stage(changes, project, `${current.root}/${question.document}`, json(selected)); + } + current.index.questionState = questionState(current.index.manifest, current.package.manifest, current.package.definitions, current.index.bindingIntent, current.index.questionState); + return { ...stageMutation(project, current, 'workspace-answer', changes), questionId, currentQuestionId: current.index.questionState.currentQuestionId }; + } finally { release(); } +} + +export function workspaceStatus(input: ObjectValue): UarWorkspaceStatus { + const workspace = loadWorkspace(input), diagnostics = workspace.migrationReceipt?.diagnostics ?? []; + const cursor = input.cursor === undefined ? 0 : Number(input.cursor), requested = input.pageSize === undefined ? DEFAULT_PAGE_SIZE : Number(input.pageSize); + if (!Number.isSafeInteger(cursor) || cursor < 0) throw new Error('cursor must be a nonnegative integer'); + if (!Number.isSafeInteger(requested) || requested < 1 || requested > MAX_PAGE_SIZE) throw new Error(`pageSize must be between 1 and ${MAX_PAGE_SIZE}`); + const items = diagnostics.slice(cursor, cursor + requested), next = cursor + items.length; + const current = workspace.index.questionState.questions.find(item => item.id === workspace.index.questionState.currentQuestionId), answered = workspace.index.questionState.questions.filter(item => item.state === 'answered').length; + return { teamId: workspace.index.teamId, revision: workspace.index.revision, packageId: workspace.index.packageId, packageVersion: workspace.index.packageVersion, profile: UAR_PROFILE_V2, + counts: { agents: workspace.package.definitions.filter(item => item.document.kind === 'AgentDefinition').length, teams: workspace.package.definitions.filter(item => item.document.kind === 'TeamDefinition').length, workflows: workspace.package.definitions.filter(item => item.document.kind === 'WorkflowDefinition').length, diagnostics: diagnostics.length }, + complete: current === undefined && !workspace.migrationReceipt?.activationBlocked, + nextQuestion: current ? { id: current.id, document: current.document, pointer: current.pointer, question: current.prompt } : null, + questions: { answered, pending: workspace.index.questionState.questions.length - answered, currentQuestionId: workspace.index.questionState.currentQuestionId }, + diagnostics: { items, cursor: next < diagnostics.length ? String(next) : null, remaining: Math.max(0, diagnostics.length - next) } }; +} + +function compareSemver(left: string, right: string): number { + const parse = (value: string): [number[], string[]] => { const [core, pre] = value.split('-', 2); return [core.split('.').map(Number), pre === undefined ? [] : pre.split('.')]; }; + const [leftCore, leftPre] = parse(left), [rightCore, rightPre] = parse(right); + for (let index = 0; index < 3; index++) if (leftCore[index] !== rightCore[index]) return leftCore[index]! - rightCore[index]!; + if (!leftPre.length || !rightPre.length) return leftPre.length ? -1 : rightPre.length ? 1 : 0; + for (let index = 0; index < Math.max(leftPre.length, rightPre.length); index++) { + const a = leftPre[index], b = rightPre[index]; if (a === undefined || b === undefined) return a === undefined ? -1 : 1; if (a === b) continue; + const an = /^[0-9]+$/.test(a), bn = /^[0-9]+$/.test(b); if (an && bn) return Number(a) - Number(b); if (an !== bn) return an ? -1 : 1; return a.localeCompare(b); + } + return 0; +} + +type RevisionTuple = { id: string; fromVersion: string; toVersion: string }; + +function replaceReferences(document: ObjectValue, tuples: RevisionTuple[]): boolean { + let changed = false; + const visit = (value: Json): void => { + if (!value || typeof value !== 'object') return; if (Array.isArray(value)) { value.forEach(visit); return; } + if (typeof value.id === 'string' && typeof value.version === 'string') { + const tuple = tuples.find(item => item.id === value.id && item.fromVersion === value.version); + if (tuple) { value.version = tuple.toVersion; delete value.digest; changed = true; } + } + Object.values(value).forEach(visit); + }; + visit(document); return changed; +} + +export function reviseWorkspace(input: ObjectValue): ObjectValue { + const project = projectRoot(input), sourcePath = workspacePath(input), destinationId = teamId(input, 'out'), destinationPath = workspacePath(input, 'out'), release = lock(project, sourcePath); + try { + const current = loadWorkspace(input); assertRevision(current, expectedRevision(input)); + const nextVersion = text(input.nextVersion, 'nextVersion'); + if (!SEMVER.test(nextVersion) || compareSemver(nextVersion, current.index.packageVersion) <= 0) throw new Error(`nextVersion must be greater than current package version ${current.index.packageVersion}`); + if (fs.existsSync(projectFile(project, `${destinationPath}/${INDEX_FILE}`))) throw new Error(`Workspace already exists: ${destinationId}`); + const definitions = current.package.definitions.map(item => ({ path: item.path, document: structuredClone(item.document), raw: readFile(projectFile(project, `${sourcePath}/${item.path}`))! })); + const edits = input.edits === undefined ? [] : input.edits; if (!Array.isArray(edits)) throw new Error('edits must be an array'); + const changedPaths = new Set(), tuples: RevisionTuple[] = []; + for (const [position, raw] of edits.entries()) { + const edit = object(raw, `edits[${position}]`), file = relativeFile(text(edit.path, `edits[${position}].path`)); + if (changedPaths.has(file)) throw new Error(`Duplicate revision edit: ${file}`); + const selected = definitions.find(item => item.path === file); if (!selected) throw new Error(`Revision edit path is not a definition: ${file}`); + const document = object(edit.document, `edits[${position}].document`); + if (document.kind !== selected.document.kind || document.id !== selected.document.id) throw new Error(`Revision edit cannot change definition kind or identity: ${file}`); + const oldVersion = text(selected.document.version, `${file}.version`), newVersion = text(document.version, `${file}.version`); + if (!SEMVER.test(newVersion) || compareSemver(newVersion, oldVersion) <= 0) throw new Error(`Edited definition ${file} requires a strictly greater semantic version`); + delete document.contentDigest; selected.document = structuredClone(document); changedPaths.add(file); tuples.push({ id: text(document.id, `${file}.id`), fromVersion: oldVersion, toVersion: newVersion }); + } + for (let pass = 0; pass <= definitions.length; pass++) { + let propagated = false; + for (const selected of definitions) { + if (!replaceReferences(selected.document, tuples) || changedPaths.has(selected.path)) continue; + const oldVersion = text(current.package.definitions.find(item => item.path === selected.path)!.document.version, `${selected.path}.version`); + if (compareSemver(nextVersion, oldVersion) <= 0) throw new Error(`Dependency update requires ${nextVersion} to exceed ${selected.path} version ${oldVersion}`); + selected.document.version = nextVersion; delete selected.document.contentDigest; changedPaths.add(selected.path); + tuples.push({ id: text(selected.document.id, `${selected.path}.id`), fromVersion: oldVersion, toVersion: nextVersion }); propagated = true; + } + if (!propagated) break; + } + const manifest = structuredClone(current.package.manifest); manifest.version = nextVersion; delete manifest.contentDigest; delete manifest.files; delete manifest.lock; replaceReferences(manifest, tuples); + compileUarPackage({ manifest, definitions: definitions.map(item => ({ path: item.path, document: item.document })) }, current.migrationReceipt); + const nextRevision = current.index.revision + 1; + const index = validateIndex({ ...current.index, revision: nextRevision, teamId: destinationId, packageVersion: nextVersion, base: { teamId: current.index.teamId, revision: current.index.revision, packageVersion: current.index.packageVersion }, questionState: questionState(current.index.manifest, manifest, definitions, current.index.bindingIntent, current.index.questionState) }); + const changes = new Map(); stage(changes, project, `${destinationPath}/${INDEX_FILE}`, json(index)); stage(changes, project, `${destinationPath}/${index.manifest}`, json(manifest)); + for (const definition of definitions) stage(changes, project, `${destinationPath}/${definition.path}`, changedPaths.has(definition.path) ? json(definition.document) : definition.raw); + if (index.migrationReceipt) stage(changes, project, `${destinationPath}/${index.migrationReceipt}`, readFile(projectFile(project, `${sourcePath}/${index.migrationReceipt}`))!); + const recovery = commitChanges(project, [...changes.values()], { operation: 'workspace-revise', teamId: destinationId, baseTeamId: current.index.teamId, beforeRevision: current.index.revision, afterRevision: nextRevision }); + return { workspace: destinationId, path: destinationPath, ...(index.base ? { base: index.base } : {}), revision: nextRevision, packageVersion: nextVersion, changedDefinitions: [...changedPaths].sort(), unchangedDefinitions: definitions.filter(item => !changedPaths.has(item.path)).map(item => item.path).sort(), recovery }; + } finally { release(); } +} diff --git a/skills/agent-team-creator/schemas/uar-migration-diagnostics.schema.json b/skills/agent-team-creator/schemas/uar-migration-diagnostics.schema.json new file mode 100644 index 0000000..e9db63f --- /dev/null +++ b/skills/agent-team-creator/schemas/uar-migration-diagnostics.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://prometheus-ags.github.io/schemas/agent-team/uar-migration-diagnostics/v1", + "title": "UAR authoring migration receipt", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "sourceProfile", "targetProfile", "diagnostics", "activationBlocked"], + "properties": { + "schemaVersion": { "const": 1 }, + "sourceProfile": { "type": "string", "minLength": 1 }, + "targetProfile": { "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" }, + "activationBlocked": { "type": "boolean" }, + "preservedSource": {}, + "diagnostics": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["sourceDocument", "sourcePointer", "disposition", "reason"], + "properties": { + "sourceDocument": { "type": "string", "minLength": 1 }, + "sourcePointer": { "type": "string", "pattern": "^(?:/(?:[^~/]|~[01])*)*$" }, + "disposition": { "enum": ["exact", "translated", "optional-unsupported", "required-unsupported"] }, + "targetDocument": { "type": "string", "minLength": 1 }, + "targetPointer": { "type": "string", "pattern": "^(?:/(?:[^~/]|~[01])*)*$" }, + "reason": { "type": "string", "minLength": 1 } + } + } + } + } +} diff --git a/skills/agent-team-creator/schemas/uar-package-authoring-v2.schema.json b/skills/agent-team-creator/schemas/uar-package-authoring-v2.schema.json new file mode 100644 index 0000000..6370323 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar-package-authoring-v2.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://prometheus-ags.github.io/schemas/agent-team/uar-package-authoring/v2", + "title": "UAR collaboration draft.2 inline authoring request", + "description": "Compatibility carrier for callers that still submit the complete graph inline. File-backed workspaces are preferred for incremental authoring.", + "type": "object", + "additionalProperties": false, + "required": ["manifest", "definitions"], + "properties": { + "manifest": { + "type": "object", + "properties": { + "profile": { "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" }, + "kind": { "const": "PackageManifest" } + }, + "required": ["profile", "kind", "id", "version"] + }, + "definitions": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path", "document"], + "properties": { + "path": { "type": "string", "pattern": "^(?!/)(?!.*\\\\)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._/-]+$" }, + "document": { + "type": "object", + "properties": { + "profile": { "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" }, + "kind": { "enum": ["AgentDefinition", "TeamDefinition", "WorkflowDefinition"] } + }, + "required": ["profile", "kind", "id", "version"] + } + } + } + } + } +} diff --git a/skills/agent-team-creator/schemas/uar-workspace.schema.json b/skills/agent-team-creator/schemas/uar-workspace.schema.json new file mode 100644 index 0000000..a9b1465 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar-workspace.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://prometheus-ags.github.io/schemas/agent-team/uar-workspace/v1", + "title": "UAR draft.2 team-scoped authoring workspace index", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "revision", "profile", "teamId", "packageId", "packageVersion", "manifest", "definitions", "questionState"], + "properties": { + "schemaVersion": { "const": 1 }, + "revision": { "type": "integer", "minimum": 1 }, + "profile": { "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" }, + "teamId": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,62}$" }, + "packageId": { "type": "string", "minLength": 1 }, + "packageVersion": { "$ref": "#/$defs/semver" }, + "manifest": { "$ref": "#/$defs/relativeFile" }, + "definitions": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/relativeFile" } }, + "migrationReceipt": { "$ref": "#/$defs/relativeFile" }, + "bindingIntent": { "enum": ["package-only", "package-and-binding"] }, + "base": { + "type": "object", + "additionalProperties": false, + "required": ["teamId", "revision", "packageVersion"], + "properties": { + "teamId": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,62}$" }, + "revision": { "type": "integer", "minimum": 1 }, + "packageVersion": { "$ref": "#/$defs/semver" } + } + }, + "questionState": { + "type": "object", + "additionalProperties": false, + "required": ["currentQuestionId", "questions"], + "properties": { + "currentQuestionId": { "type": ["string", "null"] }, + "questions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "document", "pointer", "prompt", "required", "state"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "document": { "$ref": "#/$defs/relativeFile" }, + "pointer": { "type": "string", "pattern": "^/" }, + "prompt": { "type": "string", "minLength": 1 }, + "required": { "const": true }, + "state": { "enum": ["pending", "answered"] }, + "answer": {} + } + } + } + } + } + }, + "$defs": { + "semver": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$" }, + "relativeFile": { "type": "string", "pattern": "^(?!/)(?!.*\\\\)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._/-]+$" } + } +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/agent-definition.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/agent-definition.schema.json new file mode 100644 index 0000000..f9eadcb --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/agent-definition.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/agent-definition.schema.json", + "title": "UAR AgentDefinition — official collaboration draft.2 contract checkpoint", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "AgentDefinition" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "extensions": { + "$ref": "common.schema.json#/$defs/extensions" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "minLength": 1 + }, + "whenToUse": { + "type": "string", + "minLength": 1 + }, + "instructions": { + "type": "string", + "minLength": 1 + }, + "input": { + "$ref": "common.schema.json#/$defs/contract" + }, + "output": { + "$ref": "common.schema.json#/$defs/contract" + }, + "skills": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/skill" + }, + "minItems": 0 + }, + "models": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/model" + }, + "minItems": 1 + }, + "permittedChildren": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "minItems": 0 + }, + "context": { + "$ref": "common.schema.json#/$defs/context" + }, + "requestedLimits": { + "$ref": "common.schema.json#/$defs/limits" + }, + "legacySections": { + "type": "object" + }, + "sourceDescriptor": { + "type": "object" + }, + "sourceIdentity": { + "$ref": "common.schema.json#/$defs/sourceIdentity" + }, + "renameMapping": { + "$ref": "common.schema.json#/$defs/renameMapping" + }, + "authoredFields": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/jsonPointer" + }, + "uniqueItems": true + }, + "modelRequirements": { + "$ref": "common.schema.json#/$defs/semanticRequirement" + }, + "promptDialect": { + "$ref": "common.schema.json#/$defs/semanticRequirement" + }, + "ragConfiguration": { + "$ref": "common.schema.json#/$defs/semanticRequirement" + }, + "contextStrategy": { + "$ref": "common.schema.json#/$defs/semanticRequirement" + }, + "apiHarness": { + "$ref": "common.schema.json#/$defs/semanticRequirement" + } + }, + "required": [ + "profile", + "kind", + "id", + "version", + "contentDigest", + "provenance", + "requiredCapabilities", + "extensions", + "title", + "role", + "whenToUse", + "instructions", + "input", + "output", + "skills", + "models", + "permittedChildren", + "context", + "requestedLimits", + "sourceIdentity", + "renameMapping", + "authoredFields", + "modelRequirements", + "promptDialect", + "ragConfiguration", + "contextStrategy", + "apiHarness", + "legacySections", + "sourceDescriptor" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/collaboration-document.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/collaboration-document.schema.json new file mode 100644 index 0000000..f265f60 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/collaboration-document.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/collaboration-document.schema.json", + "title": "UAR collaboration draft.2 document discriminator", + "oneOf": [ + { + "$ref": "agent-definition.schema.json" + }, + { + "$ref": "team-definition.schema.json" + }, + { + "$ref": "workflow-definition.schema.json" + }, + { + "$ref": "package-manifest.schema.json" + }, + { + "$ref": "deployment-binding.schema.json" + }, + { + "$ref": "representation-grant.schema.json" + }, + { + "$ref": "conversion-report.schema.json" + }, + { + "$ref": "effective-binding-receipt.schema.json" + }, + { + "$ref": "deployment-binding-template.schema.json" + } + ] +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/common.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/common.schema.json new file mode 100644 index 0000000..e061b4f --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/common.schema.json @@ -0,0 +1,510 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/common.schema.json", + "title": "UAR collaboration common definitions — official draft.2 contract checkpoint", + "$defs": { + "id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9:._/-]*$" + }, + "semver": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$" + }, + "digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "immutableRef": { + "type": "object", + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "digest": { + "$ref": "common.schema.json#/$defs/digest" + } + }, + "required": [ + "id", + "version", + "digest" + ], + "additionalProperties": false + }, + "extension": { + "type": "object", + "properties": { + "required": { + "type": "boolean" + }, + "value": {} + }, + "required": [ + "required", + "value" + ], + "additionalProperties": false + }, + "extensions": { + "type": "object", + "propertyNames": { + "pattern": "^[a-z][a-z0-9-]*(?:[.:/][A-Za-z0-9_-]+)+$" + }, + "additionalProperties": { + "$ref": "common.schema.json#/$defs/extension" + } + }, + "provenance": { + "type": "object", + "properties": { + "source": { + "type": "string", + "minLength": 1 + }, + "license": { + "type": "string", + "minLength": 1 + }, + "authors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + } + }, + "required": [ + "source", + "authors" + ], + "additionalProperties": false + }, + "contract": { + "type": "object", + "description": "JSON Schema 2020-12 input/output contract. Semantic validation must apply the official metaschema." + }, + "context": { + "type": "object", + "properties": { + "mode": { + "enum": [ + "none", + "selected", + "authorized-fork" + ] + }, + "artifacts": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "history": { + "enum": [ + "none", + "selected", + "authorized-summary" + ] + }, + "memoryScopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + } + }, + "required": [ + "mode", + "artifacts", + "history", + "memoryScopes" + ], + "additionalProperties": false + }, + "limits": { + "type": "object", + "properties": { + "concurrentTurns": { + "type": "integer", + "minimum": 1, + "maximum": 8 + }, + "maxMembers": { + "type": "integer", + "minimum": 1, + "maximum": 16 + }, + "maxDepth": { + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + "maxPendingTasks": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + }, + "required": [ + "concurrentTurns", + "maxMembers", + "maxDepth", + "maxPendingTasks" + ], + "additionalProperties": false + }, + "budget": { + "type": "object", + "properties": { + "maxTokens": { + "type": "integer", + "minimum": 1 + }, + "maxCostMicrounits": { + "type": "integer", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "maxElapsedSeconds": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "maxTokens", + "maxCostMicrounits", + "currency", + "maxElapsedSeconds" + ], + "additionalProperties": false + }, + "skill": { + "type": "object", + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "digest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "required": { + "type": "boolean" + }, + "config": { + "type": "object" + }, + "entrypoint": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "requiredTools": { + "type": "array", + "items": { + "$ref": "#/$defs/id" + }, + "uniqueItems": true + } + }, + "required": [ + "id", + "version", + "digest", + "required", + "config", + "entrypoint", + "requiredTools" + ], + "additionalProperties": false + }, + "model": { + "type": "object", + "properties": { + "role": { + "type": "string", + "minLength": 1 + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "preferredAliases": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + } + }, + "required": [ + "role", + "capabilities", + "preferredAliases" + ], + "additionalProperties": false + }, + "taskState": { + "enum": [ + "queued", + "ready", + "running", + "awaiting_input", + "awaiting_approval", + "reconciling", + "succeeded", + "failed", + "cancelled" + ] + }, + "instanceState": { + "enum": [ + "inactive", + "ready", + "active", + "suspended", + "draining", + "stopped" + ] + }, + "artifact": { + "type": "object", + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "mediaType": { + "type": "string", + "minLength": 1 + }, + "digest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "uri": { + "type": "string", + "format": "uri-reference" + } + }, + "required": [ + "id", + "mediaType", + "digest", + "uri" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "revision": { + "type": "integer", + "minimum": 0 + }, + "receipt": { + "enum": [ + "accepted", + "delivered", + "processed", + "rejected" + ] + }, + "jsonPointer": { + "type": "string", + "pattern": "^(?:/(?:[^~/]|~[01])*)*$" + }, + "semanticRequirement": { + "type": "object", + "properties": { + "required": { + "type": "boolean" + }, + "value": {} + }, + "required": [ + "required", + "value" + ], + "additionalProperties": false + }, + "sourceIdentity": { + "type": "object", + "properties": { + "profile": { + "type": "string", + "minLength": 1 + }, + "id": { + "$ref": "#/$defs/id" + }, + "version": { + "$ref": "#/$defs/semver" + }, + "digest": { + "$ref": "#/$defs/digest" + }, + "revision": { + "oneOf": [ + { + "$ref": "#/$defs/revision" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "profile", + "id", + "version", + "digest", + "revision" + ], + "additionalProperties": false + }, + "renameMapping": { + "type": "object", + "properties": { + "sourceId": { + "$ref": "#/$defs/id" + }, + "targetId": { + "$ref": "#/$defs/id" + }, + "reason": { + "enum": [ + "unchanged", + "heading-slug", + "explicit-rename" + ] + } + }, + "required": [ + "sourceId", + "targetId", + "reason" + ], + "additionalProperties": false + }, + "representationGrantRef": { + "type": "object", + "properties": { + "grantId": { + "$ref": "#/$defs/id" + }, + "revision": { + "$ref": "#/$defs/revision" + }, + "constraintDigest": { + "$ref": "#/$defs/digest" + } + }, + "required": [ + "grantId", + "revision", + "constraintDigest" + ], + "additionalProperties": false + }, + "privateRevisionRef": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/id" + }, + "revision": { + "$ref": "#/$defs/revision" + }, + "digest": { + "$ref": "#/$defs/digest" + } + }, + "required": [ + "id", + "revision", + "digest" + ], + "additionalProperties": false + }, + "diagnostic": { + "type": "object", + "properties": { + "pointer": { + "$ref": "#/$defs/jsonPointer" + }, + "disposition": { + "enum": [ + "exact", + "translated", + "optional-unsupported", + "required-unsupported" + ] + }, + "reasonCode": { + "type": "string", + "pattern": "^(?:[a-z][a-z0-9.-]*|TEAM_[A-Z0-9_]+)$" + }, + "message": { + "type": "string", + "minLength": 1 + }, + "effectiveBindingRef": { + "oneOf": [ + { + "$ref": "#/$defs/privateRevisionRef" + }, + { + "type": "null" + } + ] + }, + "sourceKind": { + "enum": [ + "AgentDefinition", + "TeamDefinition", + "WorkflowDefinition", + "PackageManifest", + "DeploymentBinding", + "RepresentationGrant", + "ConversionReport", + "EffectiveBindingReceipt", + "DeploymentBindingTemplate" + ] + }, + "sourceDefinition": { + "$ref": "#/$defs/immutableRef" + } + }, + "required": [ + "pointer", + "disposition", + "reasonCode", + "message", + "effectiveBindingRef" + ], + "additionalProperties": false + } + } +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/consumer-source-receipt.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/consumer-source-receipt.json new file mode 100644 index 0000000..852c89e --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/consumer-source-receipt.json @@ -0,0 +1,70 @@ +{ + "schema": "prometheus-mini.uar-schema-source-receipt/1", + "initiative": { + "change": "afc-c03-lossless-definitions-and-collaboration-document-profile", + "requirements": [ + "C03.1", + "C03.2", + "C03.3", + "C09.4" + ] + }, + "provider": { + "repository": "Prometheus-AGS/universal-agent-runtime", + "commit": "0db89a46db5bd590e5d9a9732389f41e3785bdb1", + "profile": "urn:prometheus:uar:collaboration:0.1.0-draft.2", + "sourceDirectory": "docs/agents/collaboration/v0.1.0-draft.2/schemas" + }, + "consumer": { + "repository": "prometheus-skills-mini", + "baseline": "c347043dc046f6da88b7e938f273e5152c43e76f", + "snapshotDirectory": "skills/agent-team-creator/schemas/uar/0.1.0-draft.2" + }, + "schemas": [ + { + "path": "agent-definition.schema.json", + "sha256": "a5e0573b5323accec7e25766287af897cee34f689d77a2b6eb9d9367d41f179c" + }, + { + "path": "collaboration-document.schema.json", + "sha256": "e7f8215d6e177f3ab00374df0ab34d0f19e24552af34c69aaf5b049260ccc2d1" + }, + { + "path": "common.schema.json", + "sha256": "a832c0018f11abf564b341cc0ba9ed6d87172968fe2f43bdbcb0b4fb6fcfc2ef" + }, + { + "path": "conversion-report.schema.json", + "sha256": "4a013291e1768a5f0b98f6c99d3ec746f6a863cfd55d50d73863161943fb82cb" + }, + { + "path": "deployment-binding-template.schema.json", + "sha256": "73432b4f73217b166d13013cfdda63f0b139dd191196f009fc7e629886a2c22b" + }, + { + "path": "deployment-binding.schema.json", + "sha256": "44f91ff1ebd6f6b31bd4d60dd1a928f5969033f2e218dec5d3eb36d111ad15f9" + }, + { + "path": "effective-binding-receipt.schema.json", + "sha256": "b19661c1f662e2133cd884b0104dfe0e8330f132ced8064b5c6ff89bf8aa01da" + }, + { + "path": "package-manifest.schema.json", + "sha256": "bb96cd181790f391f9a543799f93369f8c53125ba9b3f70355f5f4c06fc1bd71" + }, + { + "path": "representation-grant.schema.json", + "sha256": "b856385bb5d1ab40fdb1e7e28d62ee0c9a0843e2fe46dd9162224d70c7ed8b01" + }, + { + "path": "team-definition.schema.json", + "sha256": "3b7f1102c5d822b654dc562689b7fa6316c80c01b63d3bb3fe808d4ee30673ef" + }, + { + "path": "workflow-definition.schema.json", + "sha256": "6129578c9c54d5fd7134ea11ef149ba1d629841b205856f6b151be6ff03a9abb" + } + ], + "status": "source-contract-checkpoint-not-runtime-conformance" +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/conversion-report.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/conversion-report.schema.json new file mode 100644 index 0000000..0d5decc --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/conversion-report.schema.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/conversion-report.schema.json", + "title": "UAR ConversionReport — immutable field-level compatibility evidence", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "ConversionReport" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "extensions": { + "$ref": "common.schema.json#/$defs/extensions" + }, + "exportClass": { + "const": "portable-evidence" + }, + "source": { + "$ref": "common.schema.json#/$defs/sourceIdentity" + }, + "target": { + "type": "object", + "properties": { + "profile": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "harness": { + "type": [ + "string", + "null" + ], + "minLength": 1 + } + }, + "required": [ + "profile", + "harness" + ], + "additionalProperties": false + }, + "diagnostics": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/diagnostic" + } + }, + "activationBlocked": { + "type": "boolean" + } + }, + "required": [ + "profile", + "kind", + "id", + "version", + "contentDigest", + "provenance", + "requiredCapabilities", + "extensions", + "exportClass", + "source", + "target", + "diagnostics", + "activationBlocked" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/deployment-binding-template.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/deployment-binding-template.schema.json new file mode 100644 index 0000000..6811368 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/deployment-binding-template.schema.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/deployment-binding-template.schema.json", + "title": "UAR DeploymentBindingTemplate — sanitized non-executable export", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "DeploymentBindingTemplate" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "extensions": { + "$ref": "common.schema.json#/$defs/extensions" + }, + "exportClass": { + "const": "non-executable-template" + }, + "package": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "requestedModelBindings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "requestedAlias": { + "type": "string", + "minLength": 1 + }, + "modelRequirements": { + "type": "object" + } + }, + "required": [ + "requestedAlias", + "modelRequirements" + ], + "additionalProperties": false + } + }, + "skillRequirements": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/skill" + } + }, + "storageRequirements": { + "type": "object", + "properties": { + "backendClass": { + "type": "string", + "minLength": 1 + }, + "durableTransactions": { + "type": "boolean" + } + }, + "required": [ + "backendClass", + "durableTransactions" + ], + "additionalProperties": false + }, + "effectiveLimits": { + "$ref": "common.schema.json#/$defs/limits" + }, + "effectiveBudget": { + "$ref": "common.schema.json#/$defs/budget" + }, + "rebindFields": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/jsonPointer" + }, + "minItems": 1, + "uniqueItems": true + }, + "status": { + "const": "needs-private-binding" + } + }, + "required": [ + "profile", + "kind", + "id", + "version", + "contentDigest", + "provenance", + "requiredCapabilities", + "extensions", + "exportClass", + "package", + "requestedModelBindings", + "skillRequirements", + "storageRequirements", + "effectiveLimits", + "effectiveBudget", + "rebindFields", + "status" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/deployment-binding.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/deployment-binding.schema.json new file mode 100644 index 0000000..679a9cc --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/deployment-binding.schema.json @@ -0,0 +1,298 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/deployment-binding.schema.json", + "title": "UAR private DeploymentBinding — official collaboration draft.2 contract checkpoint", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "DeploymentBinding" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "extensions": { + "$ref": "common.schema.json#/$defs/extensions" + }, + "exportClass": { + "const": "private-installed-state" + }, + "package": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "ownerId": { + "$ref": "common.schema.json#/$defs/id" + }, + "workspaceId": { + "$ref": "common.schema.json#/$defs/id" + }, + "runtimeInstanceId": { + "$ref": "common.schema.json#/$defs/id" + }, + "serviceProfile": { + "const": "uar.service-instance/1" + }, + "workspaceLocation": { + "enum": [ + "local", + "remote" + ] + }, + "endpointRoles": { + "type": "object", + "properties": { + "runtime": { + "type": "string", + "format": "uri" + }, + "administration": { + "type": "string", + "format": "uri" + }, + "models": { + "type": "string", + "format": "uri" + }, + "console": { + "type": [ + "string", + "null" + ], + "format": "uri" + } + }, + "required": [ + "runtime", + "administration", + "models", + "console" + ], + "additionalProperties": false + }, + "runtimeCredentialRef": { + "type": "string", + "minLength": 1 + }, + "workspaceRef": { + "type": "string", + "minLength": 1 + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + }, + "modelBindings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "requestedAlias": { + "type": "string", + "minLength": 1 + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "modelId": { + "type": "string", + "minLength": 1 + }, + "credentialRef": { + "type": "string", + "minLength": 1 + }, + "profile": { + "type": "object", + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + } + }, + "required": [ + "id", + "revision" + ], + "additionalProperties": false + }, + "settingsRevision": { + "$ref": "common.schema.json#/$defs/revision" + } + }, + "required": [ + "requestedAlias", + "providerId", + "modelId", + "credentialRef" + ], + "additionalProperties": false + }, + "minItems": 0 + }, + "skillBindings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "digest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "installedLocation": { + "type": "string", + "minLength": 1 + }, + "required": { + "type": "boolean" + }, + "config": { + "type": "object" + }, + "entrypoint": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "requiredTools": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/id" + }, + "uniqueItems": true + } + }, + "required": [ + "id", + "version", + "digest", + "required", + "config", + "entrypoint", + "requiredTools", + "installedLocation" + ], + "additionalProperties": false + }, + "minItems": 0 + }, + "storage": { + "type": "object", + "properties": { + "backend": { + "type": "string", + "minLength": 1 + }, + "connectionRef": { + "type": "string", + "minLength": 1 + }, + "durableTransactions": { + "const": true + } + }, + "required": [ + "backend", + "connectionRef", + "durableTransactions" + ], + "additionalProperties": false + }, + "policyRevision": { + "type": "string", + "minLength": 1 + }, + "effectiveLimits": { + "$ref": "common.schema.json#/$defs/limits" + }, + "effectiveBudget": { + "$ref": "common.schema.json#/$defs/budget" + }, + "contextGrants": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "representationGrantRefs": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/representationGrantRef" + }, + "uniqueItems": true + }, + "status": { + "enum": [ + "inactive", + "ready", + "suspended" + ] + }, + "effectiveBindingReceiptRef": { + "oneOf": [ + { + "$ref": "common.schema.json#/$defs/privateRevisionRef" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "profile", + "kind", + "id", + "version", + "contentDigest", + "provenance", + "requiredCapabilities", + "extensions", + "exportClass", + "package", + "ownerId", + "workspaceId", + "runtimeInstanceId", + "revision", + "modelBindings", + "skillBindings", + "storage", + "policyRevision", + "effectiveLimits", + "effectiveBudget", + "contextGrants", + "representationGrantRefs", + "status", + "effectiveBindingReceiptRef" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/effective-binding-receipt.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/effective-binding-receipt.schema.json new file mode 100644 index 0000000..29b64f5 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/effective-binding-receipt.schema.json @@ -0,0 +1,153 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/effective-binding-receipt.schema.json", + "title": "UAR private EffectiveBindingReceipt — requested versus enforced semantics", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "EffectiveBindingReceipt" + }, + "exportClass": { + "const": "private-binding-evidence" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "bindingRef": { + "$ref": "common.schema.json#/$defs/privateRevisionRef" + }, + "package": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "requested": { + "type": "object" + }, + "effective": { + "type": "object" + }, + "resolvedSkills": { + "type": "array", + "items": { + "type": "object", + "properties": { + "skill": { + "$ref": "common.schema.json#/$defs/skill" + }, + "installedLocation": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "skill", + "installedLocation" + ], + "additionalProperties": false + } + }, + "resolvedModels": { + "type": "array", + "items": { + "type": "object" + } + }, + "policyRevision": { + "type": "string", + "minLength": 1 + }, + "representationGrants": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/representationGrantRef" + }, + "uniqueItems": true + }, + "runtimeCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "serviceBinding": { + "type": "object", + "properties": { + "instanceId": { "type": "string", "minLength": 1 }, + "profile": { "const": "uar.service-instance/1" }, + "workspaceLocation": { "enum": ["local", "remote"] }, + "endpoints": { + "type": "object", + "properties": { + "runtime": { "type": "string", "format": "uri" }, + "administration": { "type": "string", "format": "uri" }, + "models": { "type": "string", "format": "uri" }, + "console": { "type": ["string", "null"], "format": "uri" } + }, + "required": ["runtime", "administration", "models", "console"], + "additionalProperties": false + }, + "capabilities": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "intent": { "enum": ["new", "reattach", "migrate"] }, + "bindingId": { "type": "string", "minLength": 1 }, + "bindingRevision": { "$ref": "common.schema.json#/$defs/revision" }, + "credentialRef": { "type": "string", "minLength": 1 } + }, + "required": [ + "instanceId", + "profile", + "workspaceLocation", + "endpoints", + "capabilities", + "intent" + ], + "additionalProperties": false + }, + "diagnostics": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/diagnostic" + } + }, + "admitted": { + "type": "boolean" + }, + "createdAt": { + "$ref": "common.schema.json#/$defs/timestamp" + } + }, + "required": [ + "profile", + "kind", + "exportClass", + "id", + "revision", + "contentDigest", + "bindingRef", + "package", + "requested", + "effective", + "resolvedSkills", + "resolvedModels", + "policyRevision", + "representationGrants", + "runtimeCapabilities", + "diagnostics", + "admitted", + "createdAt" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/package-manifest.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/package-manifest.schema.json new file mode 100644 index 0000000..d61b611 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/package-manifest.schema.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/package-manifest.schema.json", + "title": "UAR collaboration package-manifest — official draft.2 contract checkpoint", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "PackageManifest" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "extensions": { + "$ref": "common.schema.json#/$defs/extensions" + }, + "entrypoints": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "minItems": 1 + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._/-]+$" + }, + "kind": { + "enum": [ + "AgentDefinition", + "TeamDefinition", + "WorkflowDefinition" + ] + }, + "definition": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "byteDigest": { + "$ref": "common.schema.json#/$defs/digest" + } + }, + "required": [ + "path", + "kind", + "definition", + "byteDigest" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "lock": { + "type": "array", + "items": { + "type": "object", + "properties": { + "requestedBy": { + "$ref": "common.schema.json#/$defs/id" + }, + "reference": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "resolvedPath": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "requestedBy", + "reference", + "resolvedPath" + ], + "additionalProperties": false + }, + "minItems": 0 + }, + "capabilityDeclarations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "capability": { + "type": "string", + "minLength": 1 + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "capability", + "required" + ], + "additionalProperties": false + }, + "minItems": 0 + }, + "resolution": { + "const": "exact-version-and-digest" + } + }, + "required": [ + "profile", + "kind", + "id", + "version", + "contentDigest", + "provenance", + "requiredCapabilities", + "extensions", + "entrypoints", + "files", + "lock", + "capabilityDeclarations", + "resolution" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/representation-grant.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/representation-grant.schema.json new file mode 100644 index 0000000..c0b7da5 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/representation-grant.schema.json @@ -0,0 +1,251 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/representation-grant.schema.json", + "title": "UAR private RepresentationGrant — authority reference contract, not C17 behavior", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "RepresentationGrant" + }, + "exportClass": { + "const": "private-authority-state" + }, + "grantId": { + "$ref": "common.schema.json#/$defs/id" + }, + "issuerPrincipalId": { + "$ref": "common.schema.json#/$defs/id" + }, + "subjectPrincipalId": { + "$ref": "common.schema.json#/$defs/id" + }, + "granteeAgentInstanceId": { + "$ref": "common.schema.json#/$defs/id" + }, + "organizationId": { + "$ref": "common.schema.json#/$defs/id" + }, + "office": { + "type": "string", + "minLength": 1 + }, + "purpose": { + "type": "string", + "minLength": 1 + }, + "audienceScopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "actionScopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "uniqueItems": true + }, + "resourceScopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "uniqueItems": true + }, + "dataScopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "approvalRequirements": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "disclosureRequirements": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "consentEvidenceRef": { + "type": "string", + "minLength": 1 + }, + "organizationalAuthorityEvidenceRef": { + "type": "string", + "minLength": 1 + }, + "revision": { + "$ref": "common.schema.json#/$defs/revision" + }, + "status": { + "enum": [ + "pending", + "active", + "suspended", + "revoked", + "expired" + ] + }, + "notBefore": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "expiresAt": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "constraintDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "revocation": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "properties": { + "revision": { + "$ref": "common.schema.json#/$defs/revision" + }, + "revokedAt": { + "$ref": "common.schema.json#/$defs/timestamp" + }, + "reason": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "revision", + "revokedAt", + "reason" + ], + "additionalProperties": false + } + ] + }, + "retention": { + "type": "object", + "properties": { + "policy": { + "type": "string", + "minLength": 1 + }, + "deleteAfter": { + "oneOf": [ + { + "$ref": "common.schema.json#/$defs/timestamp" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "policy", + "deleteAfter" + ], + "additionalProperties": false + }, + "offboarding": { + "type": "object", + "properties": { + "mode": { + "enum": [ + "revoke-immediately", + "expire-only" + ] + }, + "requiredActions": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + }, + "required": [ + "mode", + "requiredActions" + ], + "additionalProperties": false + }, + "restrictions": { + "type": "object", + "properties": { + "forbiddenClaims": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "notes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + } + }, + "required": [ + "forbiddenClaims", + "notes" + ], + "additionalProperties": false + } + }, + "required": [ + "profile", + "kind", + "exportClass", + "grantId", + "issuerPrincipalId", + "subjectPrincipalId", + "granteeAgentInstanceId", + "organizationId", + "office", + "purpose", + "audienceScopes", + "actionScopes", + "resourceScopes", + "dataScopes", + "approvalRequirements", + "disclosureRequirements", + "consentEvidenceRef", + "organizationalAuthorityEvidenceRef", + "revision", + "status", + "notBefore", + "expiresAt", + "constraintDigest", + "revocation", + "retention", + "offboarding", + "restrictions" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/team-definition.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/team-definition.schema.json new file mode 100644 index 0000000..a2dcaaa --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/team-definition.schema.json @@ -0,0 +1,224 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/team-definition.schema.json", + "title": "UAR collaboration team-definition — official draft.2 contract checkpoint", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "TeamDefinition" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "extensions": { + "$ref": "common.schema.json#/$defs/extensions" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "purpose": { + "type": "string", + "minLength": 1 + }, + "instructions": { + "description": "Optional immutable approved team guidance. Digest is SHA-256 of the exact UTF-8 text bytes; runtime enforces the 16,384-byte limit.", + "type": "object", + "properties": { + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "digest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "text": { + "type": "string", + "maxLength": 16384 + } + }, + "required": ["revision", "digest", "text"], + "additionalProperties": false + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "minLength": 1 + }, + "kind": { + "enum": [ + "agent", + "team" + ] + }, + "definition": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "min": { + "type": "integer", + "minimum": 0, + "maximum": 16 + }, + "max": { + "type": "integer", + "minimum": 1, + "maximum": 16 + }, + "responsibility": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "role", + "kind", + "definition", + "min", + "max", + "responsibility" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "coordinatorRole": { + "type": "string", + "minLength": 1 + }, + "communication": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fromRole": { + "type": "string", + "minLength": 1 + }, + "toRole": { + "type": "string", + "minLength": 1 + }, + "modes": { + "type": "array", + "items": { + "enum": [ + "queue-only", + "trigger-turn" + ] + }, + "minItems": 1 + } + }, + "required": [ + "fromRole", + "toRole", + "modes" + ], + "additionalProperties": false + }, + "minItems": 0 + }, + "taskAcceptance": { + "type": "object", + "properties": { + "mode": { + "enum": [ + "operator", + "coordinator-within-binding" + ] + }, + "allowedWorkflows": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/immutableRef" + }, + "minItems": 1 + } + }, + "required": [ + "mode", + "allowedWorkflows" + ], + "additionalProperties": false + }, + "routing": { + "type": "object", + "properties": { + "eligibilityFirst": { + "const": true + }, + "strategy": { + "const": "operator-role-capacity-cost-stable-id" + }, + "explain": { + "const": true + } + }, + "required": [ + "eligibilityFirst", + "strategy", + "explain" + ], + "additionalProperties": false + }, + "limits": { + "$ref": "common.schema.json#/$defs/limits" + }, + "budget": { + "$ref": "common.schema.json#/$defs/budget" + }, + "input": { + "$ref": "common.schema.json#/$defs/contract" + }, + "output": { + "$ref": "common.schema.json#/$defs/contract" + } + }, + "required": [ + "profile", + "kind", + "id", + "version", + "contentDigest", + "provenance", + "requiredCapabilities", + "extensions", + "title", + "purpose", + "members", + "coordinatorRole", + "communication", + "taskAcceptance", + "routing", + "limits", + "budget", + "input", + "output" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/workflow-definition.schema.json b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/workflow-definition.schema.json new file mode 100644 index 0000000..24da455 --- /dev/null +++ b/skills/agent-team-creator/schemas/uar/0.1.0-draft.2/workflow-definition.schema.json @@ -0,0 +1,164 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.prometheus-ags.dev/uar/collaboration/0.1.0-draft.2/workflow-definition.schema.json", + "title": "UAR collaboration workflow-definition — official draft.2 contract checkpoint", + "type": "object", + "properties": { + "profile": { + "const": "urn:prometheus:uar:collaboration:0.1.0-draft.2" + }, + "kind": { + "const": "WorkflowDefinition" + }, + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "version": { + "$ref": "common.schema.json#/$defs/semver" + }, + "contentDigest": { + "$ref": "common.schema.json#/$defs/digest" + }, + "provenance": { + "$ref": "common.schema.json#/$defs/provenance" + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 0 + }, + "extensions": { + "$ref": "common.schema.json#/$defs/extensions" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "input": { + "$ref": "common.schema.json#/$defs/contract" + }, + "output": { + "$ref": "common.schema.json#/$defs/contract" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "$ref": "common.schema.json#/$defs/id" + }, + "role": { + "type": "string", + "minLength": 1 + }, + "dependsOn": { + "type": "array", + "items": { + "$ref": "common.schema.json#/$defs/id" + }, + "minItems": 0 + }, + "inputMapping": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "output": { + "$ref": "common.schema.json#/$defs/contract" + }, + "effect": { + "enum": [ + "none", + "read", + "idempotent-write", + "non-idempotent-write" + ] + }, + "approval": { + "enum": [ + "none", + "current-authority", + "operator-exact-effect" + ] + }, + "retry": { + "type": "object", + "properties": { + "maxAttempts": { + "type": "integer", + "minimum": 1, + "maximum": 5 + }, + "onUnknownEffect": { + "const": "reconcile-before-retry" + } + }, + "required": [ + "maxAttempts", + "onUnknownEffect" + ], + "additionalProperties": false + }, + "completion": { + "enum": [ + "artifact", + "all-dependencies-and-artifact" + ] + }, + "instructions": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "id", + "role", + "dependsOn", + "inputMapping", + "output", + "effect", + "approval", + "retry", + "completion", + "instructions" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "failurePolicy": { + "enum": [ + "stop-dependent", + "cancel-all" + ] + }, + "maxActivations": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + }, + "required": [ + "profile", + "kind", + "id", + "version", + "contentDigest", + "provenance", + "requiredCapabilities", + "extensions", + "title", + "input", + "output", + "steps", + "failurePolicy", + "maxActivations" + ], + "additionalProperties": false +} diff --git a/skills/agent-team-creator/schemas/uar/README.md b/skills/agent-team-creator/schemas/uar/README.md index b364500..69b2889 100644 --- a/skills/agent-team-creator/schemas/uar/README.md +++ b/skills/agent-team-creator/schemas/uar/README.md @@ -1,13 +1,23 @@ # UAR collaboration schema snapshot -These files are byte-for-byte copies of the official Draft 0.1.0-draft.1 schemas -from Universal Agent Runtime commit +The files directly in this directory are byte-for-byte copies of the official +Draft `0.1.0-draft.1` schemas from Universal Agent Runtime commit `cbb511ac61eb6b2928d7a80c67324baeb913ca11`: `docs/agents/collaboration/v0.1.0-draft.1/schemas/` -They describe canonical compiled documents. The adjacent -`uar-package-authoring.schema.json` describes the pre-digest authoring envelope. -Update both full and mini mirrors together when UAR publishes a new profile; never -silently reinterpret an installed package under a newer schema. +The `0.1.0-draft.2/` directory is the byte-identical provider schema family from +UAR commit `41375cf6cd137a8a825be102c49516211c3fa2e5` at: +`docs/agents/collaboration/v0.1.0-draft.2/schemas/` + +Its accepted source paths and hashes are recorded in +`0.1.0-draft.2/consumer-source-receipt.json` and the repository-scoped OpenSpec +change evidence. The receipt is consumer provenance, not a provider schema. +Draft.1 stays available for reads and explicit +migration; it is never relabeled as draft.2. + +These snapshots describe canonical compiled documents. The adjacent authoring +and workspace schemas describe pre-digest input carriers owned by this skill. +Never modify provider-owned schema bytes or infer runtime conformance from schema +validity. Full and mini mirrors consume the same frozen provider checkpoint. diff --git a/skills/agent-team-creator/scripts/cli.mjs b/skills/agent-team-creator/scripts/cli.mjs index 2b6a215..82b0296 100755 --- a/skills/agent-team-creator/scripts/cli.mjs +++ b/skills/agent-team-creator/scripts/cli.mjs @@ -10,7 +10,7 @@ import { readState, initState, mutateState, mutateStateAsync, taskAction, comple import { createHandoff, acceptHandoff } from './handoff.mjs'; import { discoverModels, selectModel } from './models.mjs'; import { queueMemory, publishMemory } from './memory.mjs'; -import { compileUarPackage, diffUarPackages, writeUarPackage } from './uar-package.mjs'; +import { dispatchUarAuthoring, isUarAuthoringCommand, uarAuthoringCommands } from './uar-package/commands.mjs'; import { refuseUarActivation, uarBindingInstall, uarBindingPreflight, uarBindingStatus, uarCapabilities, uarPackageInstall, uarPackagePreflight, uarPackageStatus, } from './uar-client.mjs'; function revision(input) { const r = input.expectedRevision; @@ -20,6 +20,8 @@ function revision(input) { } const stateFile = (input) => path.resolve(text(input.state, 'state')); async function dispatch(command, input) { + if (isUarAuthoringCommand(command)) + return dispatchUarAuthoring(command, input); switch (command) { case 'guide': return guide(input); case 'validate': return { valid: true, team: validateTeam(input.team) }; @@ -64,9 +66,6 @@ async function dispatch(command, input) { const state = await mutateStateAsync(stateFile(input), revision(input), async (state) => { receipt = await publishMemory(state, object(input.publication, 'publication')); }); return { state, publication: receipt }; } - case 'uar-package-validate': return { valid: true, package: compileUarPackage(input.package) }; - case 'uar-package-build': return writeUarPackage(input.out, input.package); - case 'uar-package-diff': return diffUarPackages(input.before, input.after); case 'uar-capabilities': return uarCapabilities(input); case 'uar-package-preflight': return uarPackagePreflight(input); case 'uar-package-install': return uarPackageInstall(input); @@ -78,7 +77,7 @@ async function dispatch(command, input) { default: throw Error(`Unknown command: ${command}`); } } -const commands = ['guide', 'validate', 'init', 'status', 'team-update', 'export', 'install-project', 'task', 'complete-kbd', 'handoff-create', 'handoff-accept', 'models-discover', 'models-select', 'memory-queue', 'memory-publish', 'uar-package-validate', 'uar-package-build', 'uar-package-diff', 'uar-capabilities', 'uar-package-preflight', 'uar-package-install', 'uar-package-status', 'uar-binding-preflight', 'uar-binding-install', 'uar-binding-status', 'uar-activate']; +const commands = ['guide', 'validate', 'init', 'status', 'team-update', 'export', 'install-project', 'task', 'complete-kbd', 'handoff-create', 'handoff-accept', 'models-discover', 'models-select', 'memory-queue', 'memory-publish', ...uarAuthoringCommands, 'uar-capabilities', 'uar-package-preflight', 'uar-package-install', 'uar-package-status', 'uar-binding-preflight', 'uar-binding-install', 'uar-binding-status', 'uar-activate']; async function main() { if (Number(process.versions.node.split('.')[0]) < 22) throw Error('Node.js 22 or newer is required'); diff --git a/skills/agent-team-creator/scripts/guidance.mjs b/skills/agent-team-creator/scripts/guidance.mjs index 941e057..fca72ed 100755 --- a/skills/agent-team-creator/scripts/guidance.mjs +++ b/skills/agent-team-creator/scripts/guidance.mjs @@ -1,6 +1,7 @@ import { id, text, strings, target, object } from './validation.mjs'; +import { workspaceStatus } from './uar-package/workspace.mjs'; export const questions = [ - { key: 'operation', question: 'Are you creating a team, revising a versioned team, or deploying an approved package?', choices: ['create', 'revise', 'deploy'] }, + { key: 'operation', question: 'Are you creating a local team, authoring a persisted UAR workspace, revising a versioned team, or deploying an approved package?', choices: ['create', 'author', 'revise', 'deploy'] }, { key: 'id', question: 'What short name should identify this team?' }, { key: 'outcome', question: 'What should be different when this work is finished?' }, { key: 'complexity', question: 'Is this one isolated change, or work spanning several components?', choices: ['simple', 'complex'] }, @@ -35,13 +36,19 @@ const specialists = { }; export function guide(input) { const operation = (input.operation === undefined ? 'create' : String(input.operation)); - if (!['create', 'revise', 'deploy'].includes(operation)) - throw Error('operation must be create, revise, or deploy'); + if (!['create', 'author', 'revise', 'deploy'].includes(operation)) + throw Error('operation must be create, author, revise, or deploy'); + if (operation === 'author') { + const status = workspaceStatus(input); + const question = status.nextQuestion; + return { operation, ready: status.complete, missing: question ? [`${question.document}${question.pointer}`] : [], + questions: question ? [{ key: question.id, question: question.question, document: question.document, pointer: question.pointer }] : [], workspace: status }; + } if (operation === 'revise') { const required = ['state', 'changeSummary', 'nextVersion', 'deploymentIntent']; const missing = required.filter(key => input[key] === undefined); if (missing.length) - return { operation, ready: false, missing, questions: revisionQuestions }; + return { operation, ready: false, missing: [missing[0]], questions: revisionQuestions.filter(question => question.key === missing[0]) }; const nextVersion = text(input.nextVersion, 'nextVersion'); if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(nextVersion)) throw Error('nextVersion must be semantic version x.y.z'); @@ -58,7 +65,7 @@ export function guide(input) { const required = ['baseUrl', 'credentialRef', 'packageDirectory', 'bindingIntent']; const missing = required.filter(key => input[key] === undefined); if (missing.length) - return { operation, ready: false, missing, questions: deploymentQuestions }; + return { operation, ready: false, missing: [missing[0]], questions: deploymentQuestions.filter(question => question.key === missing[0]) }; if (!['package-only', 'package-and-binding'].includes(String(input.bindingIntent))) throw Error('Invalid bindingIntent'); const credentialRef = text(input.credentialRef, 'credentialRef'); @@ -68,11 +75,13 @@ export function guide(input) { connection: { baseUrl: text(input.baseUrl, 'baseUrl'), credentialRef }, packageDirectory: text(input.packageDirectory, 'packageDirectory'), bindingIntent: input.bindingIntent, sequence: ['uar-capabilities', 'uar-package-preflight', 'uar-package-install', ...(input.bindingIntent === 'package-and-binding' ? ['uar-binding-preflight', 'uar-binding-install'] : [])], - activation: 'refused-until-I2', + activation: 'not-owned-by-authoring-client', } }; } const required = ['id', 'outcome', 'complexity', 'areas', 'deliverables', 'budget', 'review', 'harness', 'scope']; const missing = required.filter(k => input[k] === undefined); + if (missing.length && input.scope === 'uar') + return { operation, questions: questions.filter(question => question.key === missing[0]), missing: [missing[0]] }; if (missing.length) return { operation, questions, missing }; const teamId = id(input.id), outcome = text(input.outcome, 'outcome'); @@ -122,26 +131,36 @@ export function guide(input) { const unresolved = roles.filter(r => r.owns.length === 0); const alternatives = ['Use one implementer for sequential work; invoke specialist skills as needed.', 'Add parallel roles only where work and file ownership can be separated.']; const skillDiscovery = 'Skill names are suggestions, not installation claims. Discover installed AgentSkills, inspect their source and requirements, and replace or remove unavailable skills before export.'; - if (unresolved.length) + if (unresolved.length) { + const pending = input.scope === 'uar' ? unresolved.slice(0, 1) : unresolved; return { operation, ready: false, proposedRoles: roles, reasons, alternatives, skillDiscovery, - missing: unresolved.map(r => 'ownership.' + r.id), - questions: unresolved.map(r => ({ key: 'ownership.' + r.id, question: 'Which project-relative files or output directories may ' + r.id + ' write? For read-only review, assign a separate findings path. Inspect the project and suggest paths instead of guessing.' })) }; + missing: pending.map(r => 'ownership.' + r.id), + questions: pending.map(r => ({ key: 'ownership.' + r.id, question: 'Which project-relative files or output directories may ' + r.id + ' write? For read-only review, assign a separate findings path. Inspect the project and suggest paths instead of guessing.' })) }; + } const team = { schemaVersion: 1, id: teamId, outcome, scope: input.scope, harness, roles, modelPolicy: { tier } }; if (input.scope === 'uar') { - const required = ['packageId', 'packageVersion', 'coordinatorRole', 'workflowSummary', 'communicationPolicy', 'aggregateLimits', 'aggregateBudget', 'bindingIntent']; + const required = ['packageId', 'packageVersion', 'coordinatorRole', 'workflowSummary', 'communicationPolicy', 'aggregateLimits', 'aggregateBudget', 'bindingIntent', 'sharedInstructions', 'requiredResources', 'executionProfile']; const missing = required.filter(key => input[key] === undefined); const uarQuestions = [ { key: 'packageId', question: 'What stable UAR package identity should own these agent, team, and workflow definitions?' }, { key: 'packageVersion', question: 'What semantic version identifies this immutable package?' }, { key: 'coordinatorRole', question: 'Which proposed role is the single fixed coordinator?' }, { key: 'workflowSummary', question: 'What finite task dependencies, outputs, effects, approvals, and retry decisions should the workflow declare?' }, - { key: 'communicationPolicy', question: 'Which explicit role-to-role paths may queue a message or trigger a turn?' }, + { key: 'communicationPolicy', question: 'Which directed role-to-role edges permit queue-only messages, explicit delegation, and worker-to-coordinator result disclosure? Reply permission requires its own reverse edge.' }, + { key: 'sharedInstructions', question: 'What shared behavioral instructions should every member receive? Use an empty string for none; this guidance cannot expand tools, credentials, policy, or resource grants.' }, + { key: 'requiredResources', question: 'Which exact skills/tools and selected inputs are required by each role, and which KB, history, or memory requests may be excluded? Unsupported required resources block runtime admission.' }, + { key: 'executionProfile', question: 'Is this package for catalog-only authoring, manual member execution, or the negotiated cooperating-pair execution profile?', choices: ['catalog-only', 'manual-member', 'cooperating-pair'] }, { key: 'aggregateLimits', question: 'What aggregate concurrent-turn, member, nesting, and pending-task limits should the team request?' }, { key: 'aggregateBudget', question: 'What aggregate token, cost, currency, and elapsed-time ceilings should the team request?' }, { key: 'bindingIntent', question: 'Should creation stop after the immutable catalog package or also prepare a private deployment binding?', choices: ['package-only', 'package-and-binding'] }, ]; if (missing.length) - return { operation, ready: false, proposedRoles: roles, reasons, alternatives, skillDiscovery, missing, questions: uarQuestions }; + return { operation, ready: false, proposedRoles: roles, reasons, alternatives, skillDiscovery, missing: [missing[0]], questions: uarQuestions.filter(question => question.key === missing[0]) }; + if (typeof input.sharedInstructions !== 'string' || Buffer.byteLength(input.sharedInstructions, 'utf8') > 16_384) + throw Error('sharedInstructions must be a string of at most 16384 UTF-8 bytes; use an empty string for none'); + if (!['catalog-only', 'manual-member', 'cooperating-pair'].includes(String(input.executionProfile))) + throw Error('Invalid executionProfile'); + const resources = object(input.requiredResources, 'requiredResources'); const packageVersion = text(input.packageVersion, 'packageVersion'); if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(packageVersion)) throw Error('packageVersion must be semantic version x.y.z'); @@ -153,8 +172,10 @@ export function guide(input) { return { operation, ready: true, questions: [], team, reasons, alternatives, skillDiscovery, maintenance: { packageId: text(input.packageId, 'packageId'), packageVersion, coordinatorRole, workflowSummary: text(input.workflowSummary, 'workflowSummary'), communicationPolicy: text(input.communicationPolicy, 'communicationPolicy'), + sharedInstructions: input.sharedInstructions, requiredResources: resources, executionProfile: input.executionProfile, + executionAuthority: 'Catalog authoring never activates members or widens private binding authority. Negotiate runtime capabilities before offering automatic cooperation.', limits: object(input.aggregateLimits, 'aggregateLimits'), budget: object(input.aggregateBudget, 'aggregateBudget'), bindingIntent: input.bindingIntent, - next: 'Author complete canonical documents with exact skill locks, model aliases, input/output contracts, and required capabilities, then run uar-package-validate.', + next: 'Initialize a persisted workspace, update one declared source document at a time, and use operation=author for one bounded next question.', } }; } return { operation, ready: true, questions: [], team, reasons, diff --git a/skills/agent-team-creator/scripts/project-files.mjs b/skills/agent-team-creator/scripts/project-files.mjs index 9619a34..2aa0da1 100644 --- a/skills/agent-team-creator/scripts/project-files.mjs +++ b/skills/agent-team-creator/scripts/project-files.mjs @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import { createHash } from 'node:crypto'; +import { createHash, randomUUID } from 'node:crypto'; import { relativeFile } from './validation.mjs'; export const hash = (text) => createHash('sha256').update(text).digest('hex'); /** Resolve existing links without allowing an instruction/config write outside the project. */ @@ -49,27 +49,58 @@ export function stage(changes, root, file, content) { if (before !== content) changes.set(absolute, { file, absolute, before, after: content }); } -export function commitChanges(root, changes) { +function atomicReplace(file, content) { + const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${randomUUID()}.tmp`); + const descriptor = fs.openSync(temporary, 'wx', 0o600); + try { + fs.writeFileSync(descriptor, content); + fs.fsyncSync(descriptor); + } + catch (error) { + fs.closeSync(descriptor); + fs.rmSync(temporary, { force: true }); + throw error; + } + fs.closeSync(descriptor); + try { + for (let attempt = 0;; attempt++) { + try { + fs.renameSync(temporary, file); + break; + } + catch (error) { + const transient = ['EPERM', 'EBUSY', 'EACCES'].includes(error.code ?? ''); + if (process.platform !== 'win32' || !transient || attempt >= 6) + throw error; + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25 * 2 ** attempt); + } + } + } + finally { + fs.rmSync(temporary, { force: true }); + } +} +export function commitChanges(root, changes, context) { if (!changes.length) return null; const receiptId = hash(JSON.stringify(changes.map(c => [c.file, c.before, c.after]))).slice(0, 24); const recovery = `.agent-team/recovery/${receiptId}.json`; const recoveryFile = projectFile(root, recovery); // Recovery is inspectable and contains exact previous bytes before the first edit. - const receipt = JSON.stringify({ schemaVersion: 1, files: changes.map(c => ({ file: path.relative(root, c.absolute).split(path.sep).join('/'), before: c.before, afterSha256: hash(c.after) })) }, null, 2) + '\n'; + const receipt = JSON.stringify({ schemaVersion: 1, ...(context ? { context } : {}), files: changes.map(c => ({ file: path.relative(root, c.absolute).split(path.sep).join('/'), before: c.before, afterSha256: hash(c.after) })) }, null, 2) + '\n'; const existing = readFile(recoveryFile); if (existing !== null && existing !== receipt) throw Error('Recovery receipt collision'); fs.mkdirSync(path.dirname(recoveryFile), { recursive: true }); if (existing === null) - fs.writeFileSync(recoveryFile, receipt, { flag: 'wx' }); + atomicReplace(recoveryFile, receipt); const written = []; try { for (const change of changes) { if (readFile(change.absolute) !== change.before) throw Error(`Project changed after preflight: ${change.file}`); fs.mkdirSync(path.dirname(change.absolute), { recursive: true }); - fs.writeFileSync(change.absolute, change.after); + atomicReplace(change.absolute, change.after); written.push(change); } } @@ -78,7 +109,7 @@ export function commitChanges(root, changes) { if (change.before === null) fs.unlinkSync(change.absolute); else - fs.writeFileSync(change.absolute, change.before); + atomicReplace(change.absolute, change.before); } throw error; } diff --git a/skills/agent-team-creator/scripts/types.mjs b/skills/agent-team-creator/scripts/types.mjs index ac41a54..5e04875 100755 --- a/skills/agent-team-creator/scripts/types.mjs +++ b/skills/agent-team-creator/scripts/types.mjs @@ -1 +1,3 @@ export const harnesses = ['uar', 'codex', 'claude', 'copilot', 'kimi', 'minimax', 'opencode', 'deepseek']; +export const UAR_PROFILE_V1 = 'urn:prometheus:uar:collaboration:0.1.0-draft.1'; +export const UAR_PROFILE_V2 = 'urn:prometheus:uar:collaboration:0.1.0-draft.2'; diff --git a/skills/agent-team-creator/scripts/uar-client.mjs b/skills/agent-team-creator/scripts/uar-client.mjs index c532a32..7c3f15c 100644 --- a/skills/agent-team-creator/scripts/uar-client.mjs +++ b/skills/agent-team-creator/scripts/uar-client.mjs @@ -46,15 +46,19 @@ async function request(connectionValue, method, route, body, extraHeaders = {}) } return payload; } -function catalogOnly(operation, response) { +function collaborationBoundary(operation, response, binding = false) { return { operation, response: response, - catalogOnly: true, + profile: 'urn:prometheus:uar:collaboration:0.1.0-draft.2', + catalogOnly: !binding, + authority: { + conferredByPackageInstallation: false, + privateBindingEvaluatedByUar: binding, + }, activation: { supported: false, - reason: 'Team activation is unavailable until the durable local-team I2 runtime is implemented.', - nextPhase: 'I2', + reason: 'This authoring client installs definitions and private bindings; runtime activation belongs to the selected UAR instance and its negotiated execution profile.', }, }; } @@ -78,7 +82,7 @@ export async function uarPackagePreflight(input) { ...command(input), manifest: compiled.manifestUtf8, files: Object.fromEntries(compiled.files.map(file => [file.path, file.contentUtf8])), }); - return catalogOnly('package-preflight', response); + return collaborationBoundary('package-preflight', response); } export async function uarPackageInstall(input) { const compiled = loadUarPackage(input.packageDirectory); @@ -86,13 +90,13 @@ export async function uarPackageInstall(input) { ...command(input), manifest: compiled.manifestUtf8, files: Object.fromEntries(compiled.files.map(file => [file.path, file.contentUtf8])), }); - return catalogOnly('package-install', response); + return collaborationBoundary('package-install', response); } export async function uarPackageStatus(input) { const id = encodeURIComponent(text(input.packageId, 'packageId')); const version = encodeURIComponent(text(input.version, 'version')); const response = await request(input.connection, 'GET', `/api/v1/collaboration/packages/${id}/versions/${version}`); - return catalogOnly('package-status', response); + return collaborationBoundary('package-status', response); } function bindingRequest(input) { const binding = compileUarBinding(input.binding); @@ -111,19 +115,19 @@ function bindingRequest(input) { export async function uarBindingPreflight(input) { const { body, workspaceId } = bindingRequest(input); const response = await request(input.connection, 'POST', '/api/v1/collaboration/deployment-bindings:preflight', body, { 'x-uar-workspace-id': workspaceId }); - return catalogOnly('binding-preflight', response); + return collaborationBoundary('binding-preflight', response, true); } export async function uarBindingInstall(input) { const { body, workspaceId } = bindingRequest(input); const response = await request(input.connection, 'POST', '/api/v1/collaboration/deployment-bindings', body, { 'x-uar-workspace-id': workspaceId }); - return catalogOnly('binding-install', response); + return collaborationBoundary('binding-install', response, true); } export async function uarBindingStatus(input) { const id = encodeURIComponent(text(input.bindingId, 'bindingId')); const workspaceId = text(input.workspaceId, 'workspaceId'); const response = await request(input.connection, 'GET', `/api/v1/collaboration/deployment-bindings/${id}`, undefined, { 'x-uar-workspace-id': workspaceId }); - return catalogOnly('binding-status', response); + return collaborationBoundary('binding-status', response, true); } export function refuseUarActivation() { - throw new Error('UAR team activation is not implemented in I1. Install definitions and an inactive or ready binding, then wait for the durable local-team I2 runtime.'); + throw new Error('This authoring client does not invoke team activation. Use the selected UAR instance through a host supporting its negotiated execution profile.'); } diff --git a/skills/agent-team-creator/scripts/uar-package.mjs b/skills/agent-team-creator/scripts/uar-package.mjs index 8bd5b69..e677de2 100644 --- a/skills/agent-team-creator/scripts/uar-package.mjs +++ b/skills/agent-team-creator/scripts/uar-package.mjs @@ -1,490 +1,7 @@ -import { createHash } from 'node:crypto'; -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { object, relativeFile, text } from './validation.mjs'; -const PROFILE = 'urn:prometheus:uar:collaboration:0.1.0-draft.1'; -const DIGEST = /^sha256:[a-f0-9]{64}$/; -const SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/; -const kinds = new Set(['AgentDefinition', 'TeamDefinition', 'WorkflowDefinition']); -const requiredByKind = { - AgentDefinition: ['provenance', 'requiredCapabilities', 'extensions', 'title', 'role', 'whenToUse', 'instructions', 'input', 'output', 'skills', 'models', 'permittedChildren', 'context', 'requestedLimits'], - TeamDefinition: ['provenance', 'requiredCapabilities', 'extensions', 'title', 'purpose', 'members', 'coordinatorRole', 'communication', 'taskAcceptance', 'routing', 'limits', 'budget', 'input', 'output'], - WorkflowDefinition: ['provenance', 'requiredCapabilities', 'extensions', 'title', 'input', 'output', 'steps', 'failurePolicy', 'maxActivations'], -}; -const allowedByKind = { - AgentDefinition: new Set(['profile', 'kind', 'id', 'version', 'contentDigest', ...requiredByKind.AgentDefinition, 'legacySections', 'sourceDescriptor']), - TeamDefinition: new Set(['profile', 'kind', 'id', 'version', 'contentDigest', ...requiredByKind.TeamDefinition]), - WorkflowDefinition: new Set(['profile', 'kind', 'id', 'version', 'contentDigest', ...requiredByKind.WorkflowDefinition]), -}; -function sha256(value) { - return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`; -} -function assertUnicode(value) { - for (let index = 0; index < value.length; index++) { - const code = value.charCodeAt(index); - if (code >= 0xd800 && code <= 0xdbff) { - const next = value.charCodeAt(index + 1); - if (next < 0xdc00 || next > 0xdfff) - throw new Error('Canonical JSON refuses an unpaired high surrogate'); - index++; - } - else if (code >= 0xdc00 && code <= 0xdfff) { - throw new Error('Canonical JSON refuses an unpaired low surrogate'); - } - } -} -function canonical(value) { - if (value === null || typeof value === 'boolean') - return JSON.stringify(value); - if (typeof value === 'number') { - if (!Number.isFinite(value)) - throw new Error('Canonical JSON requires finite numbers'); - return JSON.stringify(value); - } - if (typeof value === 'string') { - assertUnicode(value); - return JSON.stringify(value); - } - if (Array.isArray(value)) - return `[${value.map(canonical).join(',')}]`; - return `{${Object.keys(value).sort().map(key => { - assertUnicode(key); - return `${JSON.stringify(key)}:${canonical(value[key])}`; - }).join(',')}}`; -} -function cloneObject(value, label) { - return structuredClone(object(value, label)); -} -function selfDigest(document) { - const copy = structuredClone(document); - delete copy.contentDigest; - return sha256(canonical(copy)); -} -function validateDefinitionSemantics(document, kind) { - if (kind === 'AgentDefinition') { - if (!Array.isArray(document.skills)) - throw new Error('AgentDefinition.skills must be an array'); - for (const [index, entry] of document.skills.entries()) { - const skill = object(entry, `skills[${index}]`); - for (const field of ['id', 'version', 'digest']) - text(skill[field], `skills[${index}].${field}`); - if (!DIGEST.test(String(skill.digest))) - throw new Error(`skills[${index}].digest must be sha256:<64 lowercase hex>`); - if (typeof skill.required !== 'boolean') - throw new Error(`skills[${index}].required must be boolean`); - object(skill.config, `skills[${index}].config`); - } - if (!Array.isArray(document.models) || document.models.length === 0) - throw new Error('AgentDefinition.models must be a nonempty array'); - object(document.context, 'AgentDefinition.context'); - object(document.requestedLimits, 'AgentDefinition.requestedLimits'); - return; - } - if (kind === 'TeamDefinition') { - if (!Array.isArray(document.members) || document.members.length === 0) - throw new Error('TeamDefinition.members must be a nonempty array'); - const roles = new Set(); - let coordinatorIsAgent = false; - for (const [index, entry] of document.members.entries()) { - const member = object(entry, `members[${index}]`); - const role = text(member.role, `members[${index}].role`); - if (roles.has(role)) - throw new Error(`Duplicate team member role ${role}`); - roles.add(role); - if (!['agent', 'team'].includes(String(member.kind))) - throw new Error(`Invalid members[${index}].kind`); - if (!Number.isSafeInteger(member.min) || !Number.isSafeInteger(member.max) || Number(member.min) < 0 || Number(member.max) < 1 || Number(member.min) > Number(member.max)) { - throw new Error(`Invalid members[${index}] cardinality`); - } - if (role === document.coordinatorRole && member.kind === 'agent') - coordinatorIsAgent = true; - } - if (!coordinatorIsAgent) - throw new Error('coordinatorRole must name an agent member'); - if (!Array.isArray(document.communication)) - throw new Error('TeamDefinition.communication must be an array'); - for (const [index, entry] of document.communication.entries()) { - const edge = object(entry, `communication[${index}]`); - for (const field of ['fromRole', 'toRole']) - if (!roles.has(text(edge[field], `communication[${index}].${field}`))) { - throw new Error(`communication[${index}].${field} is not a member role`); - } - } - return; - } - if (!Array.isArray(document.steps) || document.steps.length === 0) - throw new Error('WorkflowDefinition.steps must be a nonempty array'); - const steps = new Map(); - for (const [index, entry] of document.steps.entries()) { - const step = object(entry, `steps[${index}]`); - const stepId = text(step.id, `steps[${index}].id`); - if (steps.has(stepId)) - throw new Error(`Duplicate workflow step ${stepId}`); - if (!Array.isArray(step.dependsOn)) - throw new Error(`steps[${index}].dependsOn must be an array`); - steps.set(stepId, step); - } - const visiting = new Set(), visited = new Set(); - const visit = (stepId) => { - if (visiting.has(stepId)) - throw new Error(`Workflow dependency cycle at ${stepId}`); - if (visited.has(stepId)) - return; - const step = steps.get(stepId); - if (!step) - throw new Error(`Unknown workflow dependency ${stepId}`); - visiting.add(stepId); - for (const dependency of step.dependsOn) - visit(text(dependency, `${stepId}.dependsOn`)); - visiting.delete(stepId); - visited.add(stepId); - }; - for (const stepId of steps.keys()) - visit(stepId); -} -function definitionIdentity(document, label) { - if (document.profile !== PROFILE) - throw new Error(`${label}.profile must be ${PROFILE}`); - const kind = text(document.kind, `${label}.kind`); - if (!kinds.has(kind)) - throw new Error(`${label}.kind is not a collaboration definition`); - for (const field of Object.keys(document)) - if (!allowedByKind[kind].has(field)) { - throw new Error(`${label} contains unknown ${kind} field ${field}`); - } - const id = text(document.id, `${label}.id`); - const version = text(document.version, `${label}.version`); - if (!SEMVER.test(version)) - throw new Error(`${label}.version must be semantic version x.y.z`); - for (const field of requiredByKind[kind]) - if (document[field] === undefined) { - throw new Error(`${label} is missing required ${kind} field ${field}`); - } - validateDefinitionSemantics(document, kind); - return { id, version, kind }; -} -function references(document) { - const owner = text(document.id, 'definition.id'); - const result = []; - const add = (value, label) => { - const reference = object(value, label); - text(reference.id, `${label}.id`); - const version = text(reference.version, `${label}.version`); - if (!SEMVER.test(version)) - throw new Error(`${label}.version must be semantic version x.y.z`); - if (reference.digest !== undefined && !DIGEST.test(text(reference.digest, `${label}.digest`))) { - throw new Error(`${label}.digest must be sha256:<64 lowercase hex>`); - } - result.push({ owner, reference }); - }; - if (document.kind === 'AgentDefinition') { - const children = document.permittedChildren; - if (!Array.isArray(children)) - throw new Error('AgentDefinition.permittedChildren must be an array'); - children.forEach((entry, index) => add(entry, `permittedChildren[${index}]`)); - } - else if (document.kind === 'TeamDefinition') { - const members = document.members; - if (!Array.isArray(members)) - throw new Error('TeamDefinition.members must be an array'); - members.forEach((entry, index) => add(object(entry, `members[${index}]`).definition, `members[${index}].definition`)); - const acceptance = object(document.taskAcceptance, 'taskAcceptance'); - const workflows = acceptance.allowedWorkflows; - if (!Array.isArray(workflows)) - throw new Error('taskAcceptance.allowedWorkflows must be an array'); - workflows.forEach((entry, index) => add(entry, `taskAcceptance.allowedWorkflows[${index}]`)); - } - return result; -} -function replaceReferences(document, resolve) { - if (document.kind === 'AgentDefinition') { - document.permittedChildren = document.permittedChildren.map(item => resolve(object(item, 'permitted child'))); - } - else if (document.kind === 'TeamDefinition') { - document.members = document.members.map(item => { - const member = cloneObject(item, 'team member'); - member.definition = resolve(object(member.definition, 'team member definition')); - return member; - }); - const acceptance = cloneObject(document.taskAcceptance, 'taskAcceptance'); - acceptance.allowedWorkflows = acceptance.allowedWorkflows.map(item => resolve(object(item, 'allowed workflow'))); - document.taskAcceptance = acceptance; - } -} -export function compileUarPackage(value) { - const source = object(value, 'authoring package'); - const manifestInput = cloneObject(source.manifest, 'package manifest'); - if (!Array.isArray(source.definitions) || source.definitions.length === 0) { - throw new Error('authoring package definitions must be a nonempty array'); - } - const definitions = source.definitions.map((entry, index) => { - const item = object(entry, `definitions[${index}]`); - const file = relativeFile(text(item.path, `definitions[${index}].path`)); - if (file === 'manifest.json') - throw new Error('manifest.json is reserved for the compiled package manifest'); - return { path: file, document: cloneObject(item.document, `definitions[${index}].document`) }; - }); - const byKey = new Map(); - const byPath = new Set(); - for (const definition of definitions) { - const identity = definitionIdentity(definition.document, definition.path); - const key = `${identity.id}\u0000${identity.version}`; - if (byKey.has(key)) - throw new Error(`Duplicate definition identity/version: ${identity.id}@${identity.version}`); - const foldedPath = definition.path.normalize('NFC').toLowerCase(); - if (byPath.has(foldedPath)) - throw new Error(`Case-insensitive definition path collision: ${definition.path}`); - byKey.set(key, definition); - byPath.add(foldedPath); - } - const compiled = new Map(); - const visiting = new Set(); - const compile = (key) => { - const done = compiled.get(key); - if (done) - return done; - if (visiting.has(key)) - throw new Error(`Definition dependency cycle at ${key.replace('\u0000', '@')}`); - const sourceDefinition = byKey.get(key); - if (!sourceDefinition) - throw new Error(`Unresolved definition reference ${key.replace('\u0000', '@')}`); - visiting.add(key); - const document = cloneObject(sourceDefinition.document, sourceDefinition.path); - const suppliedDigest = document.contentDigest; - delete document.contentDigest; - replaceReferences(document, reference => { - const referenceKey = `${text(reference.id, 'reference.id')}\u0000${text(reference.version, 'reference.version')}`; - const dependency = compile(referenceKey); - const digest = text(dependency.document.contentDigest, 'compiled dependency digest'); - if (reference.digest !== undefined && reference.digest !== digest) { - throw new Error(`Reference digest conflict for ${reference.id}@${reference.version}`); - } - return { id: reference.id, version: reference.version, digest }; - }); - const digest = selfDigest(document); - if (suppliedDigest !== undefined && suppliedDigest !== digest) { - throw new Error(`Definition contentDigest conflict for ${document.id}@${document.version}; remove the old digest before revising content`); - } - document.contentDigest = digest; - const result = { path: sourceDefinition.path, document }; - compiled.set(key, result); - visiting.delete(key); - return result; - }; - for (const key of byKey.keys()) - compile(key); - if (manifestInput.profile !== PROFILE || manifestInput.kind !== 'PackageManifest') { - throw new Error(`package manifest must use profile ${PROFILE} and kind PackageManifest`); - } - const manifestFields = new Set(['profile', 'kind', 'id', 'version', 'contentDigest', 'provenance', 'requiredCapabilities', 'extensions', 'entrypoints', 'files', 'lock', 'capabilityDeclarations', 'resolution']); - for (const field of Object.keys(manifestInput)) - if (!manifestFields.has(field)) - throw new Error(`package manifest contains unknown field ${field}`); - text(manifestInput.id, 'manifest.id'); - const manifestVersion = text(manifestInput.version, 'manifest.version'); - if (!SEMVER.test(manifestVersion)) - throw new Error('manifest.version must be semantic version x.y.z'); - for (const field of ['provenance', 'requiredCapabilities', 'extensions', 'entrypoints', 'capabilityDeclarations']) { - if (manifestInput[field] === undefined) - throw new Error(`package manifest is missing required field ${field}`); - } - if (manifestInput.resolution !== 'exact-version-and-digest') - throw new Error('manifest.resolution must be exact-version-and-digest'); - const entrypoints = manifestInput.entrypoints; - if (!Array.isArray(entrypoints) || entrypoints.length === 0) - throw new Error('manifest.entrypoints must be a nonempty array'); - manifestInput.entrypoints = entrypoints.map((entry, index) => { - const reference = object(entry, `manifest.entrypoints[${index}]`); - const key = `${text(reference.id, 'entrypoint.id')}\u0000${text(reference.version, 'entrypoint.version')}`; - const definition = compile(key); - const digest = text(definition.document.contentDigest, 'entrypoint digest'); - if (reference.digest !== undefined && reference.digest !== digest) - throw new Error(`Entrypoint digest conflict for ${reference.id}@${reference.version}`); - return { id: reference.id, version: reference.version, digest }; - }); - const files = [...compiled.values()].sort((a, b) => a.path.localeCompare(b.path)).map(definition => ({ - path: definition.path, - contentUtf8: `${JSON.stringify(definition.document, null, 2)}\n`, - })); - manifestInput.files = files.map(file => { - const definition = compiled.get(`${text(JSON.parse(file.contentUtf8).id, 'file id')}\u0000${text(JSON.parse(file.contentUtf8).version, 'file version')}`); - return { - path: file.path, - kind: definition.document.kind, - definition: { - id: definition.document.id, - version: definition.document.version, - digest: definition.document.contentDigest, - }, - byteDigest: sha256(file.contentUtf8), - }; - }); - const locks = []; - for (const definition of compiled.values()) - for (const item of references(definition.document)) { - const key = `${item.reference.id}\u0000${item.reference.version}`; - const resolved = compiled.get(key); - if (!resolved) - throw new Error(`Unresolved compiled reference ${item.reference.id}@${item.reference.version}`); - locks.push({ - requestedBy: item.owner, - reference: item.reference, - resolvedPath: resolved.path, - }); - } - manifestInput.lock = locks.sort((a, b) => String(a.requestedBy).localeCompare(String(b.requestedBy)) || - String(object(a.reference).id).localeCompare(String(object(b.reference).id))); - const suppliedManifestDigest = manifestInput.contentDigest; - delete manifestInput.contentDigest; - const digest = selfDigest(manifestInput); - if (suppliedManifestDigest !== undefined && suppliedManifestDigest !== digest) { - throw new Error('PackageManifest contentDigest conflict; remove the old digest before revising content'); - } - manifestInput.contentDigest = digest; - return { manifest: manifestInput, manifestUtf8: `${JSON.stringify(manifestInput, null, 2)}\n`, files }; -} -export function compileUarBinding(value) { - const binding = cloneObject(value, 'deployment binding'); - if (binding.profile !== PROFILE || binding.kind !== 'DeploymentBinding') { - throw new Error(`deployment binding must use profile ${PROFILE} and kind DeploymentBinding`); - } - const required = [ - 'id', 'version', 'provenance', 'requiredCapabilities', 'extensions', 'exportClass', 'package', 'ownerId', 'workspaceId', - 'runtimeInstanceId', 'revision', 'modelBindings', 'skillBindings', 'storage', 'policyRevision', 'effectiveLimits', - 'effectiveBudget', 'contextGrants', 'representationGrantRefs', 'status', - ]; - const allowed = new Set(['profile', 'kind', 'contentDigest', ...required]); - for (const field of Object.keys(binding)) - if (!allowed.has(field)) - throw new Error(`deployment binding contains unknown field ${field}`); - for (const field of required) - if (binding[field] === undefined) - throw new Error(`deployment binding is missing required field ${field}`); - if (binding.exportClass !== 'private-installed-state') - throw new Error('deployment binding exportClass must be private-installed-state'); - if (!SEMVER.test(text(binding.version, 'binding.version'))) - throw new Error('binding.version must be semantic version x.y.z'); - for (const field of ['id', 'ownerId', 'workspaceId', 'runtimeInstanceId', 'policyRevision']) - text(binding[field], `binding.${field}`); - object(binding.provenance, 'binding.provenance'); - object(binding.extensions, 'binding.extensions'); - if (!['inactive', 'ready', 'suspended'].includes(String(binding.status))) - throw new Error('Invalid deployment binding status'); - if (!Number.isSafeInteger(binding.revision) || Number(binding.revision) < 0) - throw new Error('binding.revision must be a nonnegative safe integer'); - const packageRef = object(binding.package, 'binding.package'); - for (const field of ['id', 'version', 'digest']) - text(packageRef[field], `binding.package.${field}`); - if (!DIGEST.test(String(packageRef.digest))) - throw new Error('binding.package.digest must be sha256:<64 lowercase hex>'); - if (!Array.isArray(binding.modelBindings) || binding.modelBindings.length === 0) - throw new Error('binding.modelBindings must be a nonempty array'); - for (const field of ['requiredCapabilities', 'skillBindings', 'contextGrants', 'representationGrantRefs']) { - if (!Array.isArray(binding[field])) - throw new Error(`binding.${field} must be an array`); - } - for (const [index, entry] of binding.modelBindings.entries()) { - const model = object(entry, `binding.modelBindings[${index}]`); - for (const field of ['requestedAlias', 'providerId', 'modelId', 'credentialRef']) - text(model[field], `binding.modelBindings[${index}].${field}`); - if (/^(?:bearer|token|secret|password|api[-_]?key):/i.test(String(model.credentialRef))) { - throw new Error(`binding.modelBindings[${index}].credentialRef must be an opaque host reference, not a credential value`); - } - } - const storage = object(binding.storage, 'binding.storage'); - for (const field of ['backend', 'connectionRef']) - text(storage[field], `binding.storage.${field}`); - if (storage.durableTransactions !== true) - throw new Error('binding.storage.durableTransactions must be true'); - object(binding.effectiveLimits, 'binding.effectiveLimits'); - object(binding.effectiveBudget, 'binding.effectiveBudget'); - const suppliedDigest = binding.contentDigest; - delete binding.contentDigest; - const digest = selfDigest(binding); - if (suppliedDigest !== undefined && suppliedDigest !== digest) - throw new Error('DeploymentBinding contentDigest conflict'); - binding.contentDigest = digest; - return binding; -} -export function writeUarPackage(directoryValue, value) { - const compiled = compileUarPackage(value); - const directory = path.resolve(text(directoryValue, 'out')); - mkdirSync(path.dirname(directory), { recursive: true }); - mkdirSync(directory); - for (const file of compiled.files) { - const destination = path.join(directory, ...file.path.split('/')); - mkdirSync(path.dirname(destination), { recursive: true }); - writeFileSync(destination, file.contentUtf8, { flag: 'wx', mode: 0o600 }); - } - writeFileSync(path.join(directory, 'manifest.json'), compiled.manifestUtf8, { flag: 'wx', mode: 0o600 }); - return { directory, manifest: compiled.manifest, files: ['manifest.json', ...compiled.files.map(file => file.path)] }; -} -export function loadUarPackage(directoryValue) { - const directory = path.resolve(text(directoryValue, 'packageDirectory')); - const manifestUtf8 = readFileSync(path.join(directory, 'manifest.json'), 'utf8'); - const manifest = cloneObject(JSON.parse(manifestUtf8), 'manifest'); - if (!Array.isArray(manifest.files)) - throw new Error('Compiled package manifest.files must be an array'); - const files = manifest.files.map((entry, index) => { - const item = object(entry, `manifest.files[${index}]`); - const file = relativeFile(text(item.path, `manifest.files[${index}].path`)); - const contentUtf8 = readFileSync(path.join(directory, ...file.split('/')), 'utf8'); - if (sha256(contentUtf8) !== item.byteDigest) - throw new Error(`Package byte digest mismatch: ${file}`); - const document = object(JSON.parse(contentUtf8), file); - if (selfDigest(document) !== document.contentDigest) - throw new Error(`Package content digest mismatch: ${file}`); - return { path: file, contentUtf8 }; - }); - if (selfDigest(manifest) !== manifest.contentDigest) - throw new Error('Package manifest content digest mismatch'); - return { manifest, manifestUtf8, files }; -} -function flatten(value, prefix = '') { - const result = new Map(); - const walk = (item, current) => { - if (item && typeof item === 'object') { - if (Array.isArray(item)) - item.forEach((child, index) => walk(child, `${current}/${index}`)); - else - Object.keys(item).sort().forEach(key => walk(item[key], `${current}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`)); - } - else - result.set(current || '/', canonical(item)); - }; - walk(value, prefix); - return result; -} -export function diffUarPackages(beforeValue, afterValue) { - const before = compileUarPackage(beforeValue); - const after = compileUarPackage(afterValue); - const beforeFiles = new Map(before.files.map(file => [file.path, JSON.parse(file.contentUtf8)])); - const afterFiles = new Map(after.files.map(file => [file.path, JSON.parse(file.contentUtf8)])); - const added = [...afterFiles.keys()].filter(file => !beforeFiles.has(file)).sort(); - const removed = [...beforeFiles.keys()].filter(file => !afterFiles.has(file)).sort(); - const changes = []; - for (const file of [...beforeFiles.keys()].filter(file => afterFiles.has(file)).sort()) { - const left = flatten(beforeFiles.get(file)); - const right = flatten(afterFiles.get(file)); - const paths = [...new Set([...left.keys(), ...right.keys()])].filter(key => left.get(key) !== right.get(key)).sort(); - if (paths.length) - changes.push({ file, paths }); - } - const beforeId = text(before.manifest.id, 'before manifest id'); - const afterId = text(after.manifest.id, 'after manifest id'); - if (beforeId !== afterId) - throw new Error('Package maintenance cannot change package identity'); - const sameVersion = before.manifest.version === after.manifest.version; - if (sameVersion && before.manifest.contentDigest !== after.manifest.contentDigest) { - throw new Error('Changed immutable package content requires a new semantic version'); - } - return { - packageId: beforeId, - fromVersion: before.manifest.version, - toVersion: after.manifest.version, - added, - removed, - changed: changes, - unchanged: before.manifest.contentDigest === after.manifest.contentDigest, - }; -} +export { compileUarPackage } from './uar-package/compiler.mjs'; +export { compileUarBinding } from './uar-package/binding.mjs'; +export { writeUarPackage, loadUarPackage, compiledAsAuthoring } from './uar-package/package-files.mjs'; +export { diffUarPackages, diffUarDirectories } from './uar-package/maintenance.mjs'; +export { normalizeAuthoring, assertMigrationAllowsBuild } from './uar-package/migration.mjs'; +export { initializeWorkspace, loadWorkspace, reviseWorkspace, updateWorkspaceDocument, workspaceStatus } from './uar-package/workspace.mjs'; +export { profileSchemaInfo, validateProfileDocument } from './uar-package/profile-validation.mjs'; diff --git a/skills/agent-team-creator/scripts/uar-package/binding.mjs b/skills/agent-team-creator/scripts/uar-package/binding.mjs new file mode 100644 index 0000000..0880736 --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/binding.mjs @@ -0,0 +1,38 @@ +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { object, text } from '../validation.mjs'; +import { cloneObject, selfDigest } from './canonical.mjs'; +import { assertBindingContainsReferencesOnly, assertOpaqueReference } from './private-authority.mjs'; +import { validateProfileDocument } from './profile-validation.mjs'; +export function compileUarBinding(value) { + const binding = cloneObject(value, 'deployment binding'); + if (binding.profile !== UAR_PROFILE_V2 || binding.kind !== 'DeploymentBinding') + throw new Error(`deployment binding must use profile ${UAR_PROFILE_V2} and kind DeploymentBinding`); + assertBindingContainsReferencesOnly(binding); + const packageReference = object(binding.package, 'binding.package'); + for (const field of ['id', 'version', 'digest']) + text(packageReference[field], `binding.package.${field}`); + if (!Array.isArray(binding.modelBindings) || binding.modelBindings.length === 0) + throw new Error('binding.modelBindings must be a nonempty array'); + binding.modelBindings.forEach((entry, index) => { + const model = object(entry, `binding.modelBindings[${index}]`); + assertOpaqueReference(model.credentialRef, `/modelBindings/${index}/credentialRef`); + }); + const storage = object(binding.storage, 'binding.storage'); + assertOpaqueReference(storage.connectionRef, '/storage/connectionRef'); + if (!Array.isArray(binding.representationGrantRefs)) + throw new Error('binding.representationGrantRefs must be an array'); + binding.representationGrantRefs.forEach((entry, index) => { + const reference = object(entry, `binding.representationGrantRefs[${index}]`); + text(reference.id, `binding.representationGrantRefs[${index}].id`); + if (!Number.isSafeInteger(reference.revision) || Number(reference.revision) < 0) + throw new Error(`binding.representationGrantRefs[${index}].revision must be a nonnegative integer`); + }); + const supplied = binding.contentDigest; + delete binding.contentDigest; + const digest = selfDigest(binding); + if (supplied !== undefined && supplied !== digest) + throw new Error('DeploymentBinding contentDigest conflict'); + binding.contentDigest = digest; + validateProfileDocument(binding); + return binding; +} diff --git a/skills/agent-team-creator/scripts/uar-package/canonical.mjs b/skills/agent-team-creator/scripts/uar-package/canonical.mjs new file mode 100644 index 0000000..a6febb2 --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/canonical.mjs @@ -0,0 +1,65 @@ +import { createHash } from 'node:crypto'; +import { object } from '../validation.mjs'; +export const DIGEST = /^sha256:[a-f0-9]{64}$/; +export const SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/; +export function sha256(value) { + return `sha256:${createHash('sha256').update(value).digest('hex')}`; +} +function assertUnicode(value) { + for (let index = 0; index < value.length; index++) { + const code = value.charCodeAt(index); + if (code >= 0xd800 && code <= 0xdbff) { + const next = value.charCodeAt(index + 1); + if (next < 0xdc00 || next > 0xdfff) + throw new Error('Canonical JSON refuses an unpaired high surrogate'); + index++; + } + else if (code >= 0xdc00 && code <= 0xdfff) + throw new Error('Canonical JSON refuses an unpaired low surrogate'); + } +} +export function canonical(value) { + if (value === null || typeof value === 'boolean') + return JSON.stringify(value); + if (typeof value === 'number') { + if (!Number.isFinite(value)) + throw new Error('Canonical JSON requires finite numbers'); + return JSON.stringify(value); + } + if (typeof value === 'string') { + assertUnicode(value); + return JSON.stringify(value); + } + if (Array.isArray(value)) + return `[${value.map(canonical).join(',')}]`; + return `{${Object.keys(value).sort().map(key => { + assertUnicode(key); + return `${JSON.stringify(key)}:${canonical(value[key])}`; + }).join(',')}}`; +} +export function cloneObject(value, label) { + return structuredClone(object(value, label)); +} +export function selfDigest(document) { + const copy = structuredClone(document); + delete copy.contentDigest; + return sha256(canonical(copy)); +} +export function pointerSegment(value) { + return value.replaceAll('~', '~0').replaceAll('/', '~1'); +} +export function flatten(value, prefix = '') { + const result = new Map(); + const walk = (item, current) => { + if (item && typeof item === 'object') { + if (Array.isArray(item)) + item.forEach((child, index) => walk(child, `${current}/${index}`)); + else + Object.keys(item).sort().forEach(key => walk(item[key], `${current}/${pointerSegment(key)}`)); + } + else + result.set(current || '/', canonical(item)); + }; + walk(value, prefix); + return result; +} diff --git a/skills/agent-team-creator/scripts/uar-package/commands.mjs b/skills/agent-team-creator/scripts/uar-package/commands.mjs new file mode 100644 index 0000000..27c5277 --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/commands.mjs @@ -0,0 +1,60 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { relativeFile, text } from '../validation.mjs'; +import { projectFile } from '../project-files.mjs'; +import { compileUarPackage } from './compiler.mjs'; +import { diffUarDirectories, diffUarPackages } from './maintenance.mjs'; +import { writeUarPackage } from './package-files.mjs'; +import { profileSchemaInfo } from './profile-validation.mjs'; +import { answerWorkspaceQuestion, initializeWorkspace, loadWorkspace, reviseWorkspace, updateWorkspaceDocument, workspaceStatus } from './workspace.mjs'; +export const uarAuthoringCommands = [ + 'uar-workspace-init', 'uar-workspace-migrate', 'uar-workspace-status', 'uar-workspace-answer', 'uar-workspace-update', 'uar-workspace-revise', + 'uar-package-schema-info', 'uar-package-validate', 'uar-package-build', 'uar-package-diff', +]; +function source(input) { + if (input.workspace !== undefined) { + const loaded = loadWorkspace(input); + return { package: loaded.package, ...(loaded.migrationReceipt ? { receipt: loaded.migrationReceipt } : {}) }; + } + return { package: input.package }; +} +function outputDirectory(input) { + if (input.project === undefined) + return text(input.out, 'out'); + const project = fs.realpathSync(path.resolve(text(input.project, 'project'))); + return projectFile(project, relativeFile(text(input.out, 'out'))); +} +export function dispatchUarAuthoring(command, input) { + switch (command) { + case 'uar-workspace-init': + case 'uar-workspace-migrate': return initializeWorkspace(input); + case 'uar-workspace-status': return workspaceStatus(input); + case 'uar-workspace-answer': return answerWorkspaceQuestion(input); + case 'uar-workspace-update': return updateWorkspaceDocument(input); + case 'uar-workspace-revise': return reviseWorkspace(input); + case 'uar-package-schema-info': return profileSchemaInfo(); + case 'uar-package-validate': { + const selected = source(input); + return { valid: true, package: compileUarPackage(selected.package, selected.receipt) }; + } + case 'uar-package-build': { + const selected = source(input); + return writeUarPackage(outputDirectory(input), selected.package, selected.receipt); + } + case 'uar-package-diff': { + if (input.beforeDirectory !== undefined || input.afterDirectory !== undefined) + return diffUarDirectories(input.beforeDirectory, input.afterDirectory); + if (input.beforeWorkspace !== undefined || input.afterWorkspace !== undefined) { + const project = text(input.project, 'project'); + const before = loadWorkspace({ project, workspace: text(input.beforeWorkspace, 'beforeWorkspace') }); + const after = loadWorkspace({ project, workspace: text(input.afterWorkspace, 'afterWorkspace') }); + return diffUarPackages(before.package, after.package); + } + return diffUarPackages(input.before, input.after); + } + default: throw new Error(`Unknown UAR authoring command: ${command}`); + } +} +export function isUarAuthoringCommand(command) { + return uarAuthoringCommands.includes(command); +} diff --git a/skills/agent-team-creator/scripts/uar-package/compiler.mjs b/skills/agent-team-creator/scripts/uar-package/compiler.mjs new file mode 100644 index 0000000..77847f6 --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/compiler.mjs @@ -0,0 +1,170 @@ +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { object, relativeFile, text } from '../validation.mjs'; +import { cloneObject, DIGEST, selfDigest, SEMVER, sha256 } from './canonical.mjs'; +import { validateGraph } from './graph-validation.mjs'; +import { assertMigrationAllowsBuild, normalizeAuthoring } from './migration.mjs'; +import { assertPortable } from './private-authority.mjs'; +import { validateProfileDocument } from './profile-validation.mjs'; +const kinds = new Set(['AgentDefinition', 'TeamDefinition', 'WorkflowDefinition']); +function identity(document, label) { + if (document.profile !== UAR_PROFILE_V2) + throw new Error(`${label}.profile must be ${UAR_PROFILE_V2}`); + const kind = text(document.kind, `${label}.kind`); + if (!kinds.has(kind)) + throw new Error(`${label}.kind is not a portable collaboration definition`); + const id = text(document.id, `${label}.id`), version = text(document.version, `${label}.version`); + if (!SEMVER.test(version)) + throw new Error(`${label}.version must be semantic version x.y.z`); + return { id, version, kind }; +} +function referenceKey(value, label) { + const id = text(value.id, `${label}.id`), version = text(value.version, `${label}.version`); + if (!SEMVER.test(version)) + throw new Error(`${label}.version must be semantic version x.y.z`); + if (value.digest !== undefined && !DIGEST.test(text(value.digest, `${label}.digest`))) + throw new Error(`${label}.digest must be sha256:<64 lowercase hex>`); + return `${id}\u0000${version}`; +} +function replaceReferences(document, resolve) { + if (document.kind === 'AgentDefinition') { + if (!Array.isArray(document.permittedChildren)) + throw new Error('AgentDefinition.permittedChildren must be an array'); + document.permittedChildren = document.permittedChildren.map((item, index) => resolve(object(item, `permittedChildren[${index}]`), `/permittedChildren/${index}`)); + } + if (document.kind === 'TeamDefinition') { + if (!Array.isArray(document.members)) + throw new Error('TeamDefinition.members must be an array'); + document.members = document.members.map((item, index) => { + const member = cloneObject(item, `members[${index}]`); + member.definition = resolve(object(member.definition, `members[${index}].definition`), `/members/${index}/definition`); + return member; + }); + const acceptance = cloneObject(document.taskAcceptance, 'taskAcceptance'); + if (!Array.isArray(acceptance.allowedWorkflows)) + throw new Error('taskAcceptance.allowedWorkflows must be an array'); + acceptance.allowedWorkflows = acceptance.allowedWorkflows.map((item, index) => resolve(object(item, `taskAcceptance.allowedWorkflows[${index}]`), `/taskAcceptance/allowedWorkflows/${index}`)); + document.taskAcceptance = acceptance; + } +} +function references(document) { + const result = []; + if (document.kind === 'AgentDefinition') + document.permittedChildren.forEach((item, index) => result.push({ pointer: `/permittedChildren/${index}`, reference: object(item) })); + if (document.kind === 'TeamDefinition') { + document.members.forEach((item, index) => result.push({ pointer: `/members/${index}/definition`, reference: object(object(item).definition) })); + const acceptance = object(document.taskAcceptance); + acceptance.allowedWorkflows.forEach((item, index) => result.push({ pointer: `/taskAcceptance/allowedWorkflows/${index}`, reference: object(item) })); + } + return result; +} +export function compileUarPackage(value, workspaceReceipt) { + const normalized = normalizeAuthoring(value); + const receipt = workspaceReceipt ?? normalized.receipt; + assertMigrationAllowsBuild(receipt); + const manifestInput = cloneObject(normalized.package.manifest, 'package manifest'); + if (manifestInput.profile !== UAR_PROFILE_V2 || manifestInput.kind !== 'PackageManifest') + throw new Error(`package manifest must use profile ${UAR_PROFILE_V2} and kind PackageManifest`); + assertPortable(manifestInput, 'manifest.json'); + if (!Array.isArray(normalized.package.definitions) || normalized.package.definitions.length === 0) + throw new Error('authoring package definitions must be a nonempty array'); + const definitions = normalized.package.definitions.map((source, index) => { + const file = relativeFile(text(source.path, `definitions[${index}].path`)); + if (file === 'manifest.json') + throw new Error('manifest.json is reserved for the compiled package manifest'); + const document = cloneObject(source.document, file); + assertPortable(document, file); + return { path: file, document }; + }); + const byKey = new Map(), paths = new Set(); + for (const definition of definitions) { + const found = identity(definition.document, definition.path); + const key = `${found.id}\u0000${found.version}`; + if (byKey.has(key)) + throw new Error(`Duplicate definition identity/version: ${found.id}@${found.version}`); + const folded = definition.path.normalize('NFC').toLocaleLowerCase('en-US'); + if (paths.has(folded)) + throw new Error(`Case-insensitive definition path collision: ${definition.path}`); + byKey.set(key, definition); + paths.add(folded); + } + const compiled = new Map(), visiting = new Set(); + const compile = (key) => { + const complete = compiled.get(key); + if (complete) + return complete; + if (visiting.has(key)) + throw new Error(`Definition dependency cycle at ${key.replace('\u0000', '@')}`); + const source = byKey.get(key); + if (!source) + throw new Error(`Unresolved definition reference ${key.replace('\u0000', '@')}`); + visiting.add(key); + const document = cloneObject(source.document, source.path), supplied = document.contentDigest; + delete document.contentDigest; + replaceReferences(document, (reference, pointer) => { + const dependency = compile(referenceKey(reference, `${source.path}${pointer}`)); + const digest = text(dependency.document.contentDigest, `${dependency.path}.contentDigest`); + if (reference.digest !== undefined && reference.digest !== digest) + throw new Error(`${source.path}${pointer}/digest conflicts with resolved definition`); + return { id: reference.id, version: reference.version, digest }; + }); + const digest = selfDigest(document); + if (supplied !== undefined && supplied !== digest) + throw new Error(`Definition contentDigest conflict for ${document.id}@${document.version}; use a new version for changed content`); + document.contentDigest = digest; + validateProfileDocument(document); + if (document.kind === 'TeamDefinition' && document.instructions !== undefined) { + const instructions = object(document.instructions, `${source.path}/instructions`); + const guidance = instructions.text; // Shape and nonempty text were checked by the provider schema. + if (Buffer.byteLength(guidance, 'utf8') > 16_384) + throw new Error(`${source.path}/instructions/text exceeds 16384 UTF-8 bytes`); + if (instructions.digest !== sha256(guidance)) + throw new Error(`${source.path}/instructions/digest does not match exact UTF-8 text`); + } + const result = { path: source.path, document }; + compiled.set(key, result); + visiting.delete(key); + return result; + }; + for (const key of byKey.keys()) + compile(key); + text(manifestInput.id, 'manifest.id'); + if (!SEMVER.test(text(manifestInput.version, 'manifest.version'))) + throw new Error('manifest.version must be semantic version x.y.z'); + if (manifestInput.resolution !== 'exact-version-and-digest') + throw new Error('manifest.resolution must be exact-version-and-digest'); + if (!Array.isArray(manifestInput.entrypoints)) + throw new Error('manifest.entrypoints must be an array'); + manifestInput.entrypoints = manifestInput.entrypoints.map((entry, index) => { + const reference = object(entry, `manifest.entrypoints[${index}]`); + const definition = compile(referenceKey(reference, `manifest.entrypoints[${index}]`)); + const digest = text(definition.document.contentDigest, `${definition.path}.contentDigest`); + if (reference.digest !== undefined && reference.digest !== digest) + throw new Error(`Entrypoint digest conflict for ${reference.id}@${reference.version}`); + return { id: reference.id, version: reference.version, digest }; + }); + const files = [...compiled.values()].sort((a, b) => a.path.localeCompare(b.path)).map(definition => ({ + path: definition.path, contentUtf8: `${JSON.stringify(definition.document, null, 2)}\n`, + })); + manifestInput.files = files.map(file => { + const document = object(JSON.parse(file.contentUtf8)); + return { path: file.path, kind: document.kind, definition: { id: document.id, version: document.version, digest: document.contentDigest }, byteDigest: sha256(file.contentUtf8) }; + }); + const locks = []; + for (const definition of compiled.values()) + for (const item of references(definition.document)) { + const resolved = compiled.get(referenceKey(item.reference, `${definition.path}${item.pointer}`)); + if (!resolved) + throw new Error(`Unresolved compiled reference at ${definition.path}${item.pointer}`); + locks.push({ requestedBy: definition.document.id, reference: item.reference, resolvedPath: resolved.path }); + } + manifestInput.lock = locks.sort((left, right) => String(left.requestedBy).localeCompare(String(right.requestedBy)) || String(object(left.reference).id).localeCompare(String(object(right.reference).id))); + const suppliedManifestDigest = manifestInput.contentDigest; + delete manifestInput.contentDigest; + const manifestDigest = selfDigest(manifestInput); + if (suppliedManifestDigest !== undefined && suppliedManifestDigest !== manifestDigest) + throw new Error('PackageManifest contentDigest conflict; changed content requires a new version'); + manifestInput.contentDigest = manifestDigest; + validateProfileDocument(manifestInput); + validateGraph(manifestInput, [...compiled.values()]); + return { manifest: manifestInput, manifestUtf8: `${JSON.stringify(manifestInput, null, 2)}\n`, files }; +} diff --git a/skills/agent-team-creator/scripts/uar-package/graph-validation.mjs b/skills/agent-team-creator/scripts/uar-package/graph-validation.mjs new file mode 100644 index 0000000..cc5a07f --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/graph-validation.mjs @@ -0,0 +1,174 @@ +import { object, text } from '../validation.mjs'; +import { DIGEST, SEMVER } from './canonical.mjs'; +const keyOf = (value, label) => { + const id = text(value.id, `${label}.id`); + const version = text(value.version, `${label}.version`); + if (!SEMVER.test(version)) + throw new Error(`${label}.version must be semantic version x.y.z`); + if (value.digest !== undefined && !DIGEST.test(text(value.digest, `${label}.digest`))) + throw new Error(`${label}.digest must be sha256:<64 lowercase hex>`); + return `${id}\u0000${version}`; +}; +export function indexDefinitions(definitions) { + const result = new Map(); + const paths = new Set(); + for (const definition of definitions) { + const kind = String(definition.document.kind); + if (!['AgentDefinition', 'TeamDefinition', 'WorkflowDefinition'].includes(kind)) + throw new Error(`${definition.path}.kind is not a portable collaboration definition`); + const key = keyOf(definition.document, definition.path); + if (result.has(key)) + throw new Error(`Duplicate definition identity/version: ${key.replace('\u0000', '@')}`); + const folded = definition.path.normalize('NFC').toLocaleLowerCase('en-US'); + if (paths.has(folded)) + throw new Error(`Case-insensitive definition path collision: ${definition.path}`); + paths.add(folded); + result.set(key, { ...definition, kind, key }); + } + return result; +} +function resolve(index, value, expected, pointer) { + const reference = object(value, pointer); + const key = keyOf(reference, pointer); + const found = index.get(key); + if (!found) + throw new Error(`${pointer} references unresolved ${key.replace('\u0000', '@')}`); + if (found.kind !== expected) + throw new Error(`${pointer} expected ${expected}, observed ${found.kind}`); + const actualDigest = text(found.document.contentDigest, `${found.path}.contentDigest`); + if (reference.digest !== actualDigest) + throw new Error(`${pointer}.digest does not exactly resolve ${key.replace('\u0000', '@')}`); + return found; +} +export function validateGraph(manifest, definitions) { + const index = indexDefinitions(definitions); + if (!Array.isArray(manifest.entrypoints) || manifest.entrypoints.length !== 1) + throw new Error('/entrypoints must contain exactly one top-level TeamDefinition'); + const root = resolve(index, manifest.entrypoints[0], 'TeamDefinition', '/entrypoints/0'); + const dependencies = new Map(); + for (const definition of index.values()) { + const document = definition.document; + const edges = []; + if (definition.kind === 'AgentDefinition') { + if (!Array.isArray(document.permittedChildren)) + throw new Error(`${definition.path}/permittedChildren must be an array`); + document.permittedChildren.forEach((entry, position) => { + const pointer = `${definition.path}/permittedChildren/${position}`; + edges.push({ key: resolve(index, entry, 'AgentDefinition', pointer).key, pointer }); + }); + } + if (definition.kind === 'TeamDefinition') { + if (!Array.isArray(document.members) || document.members.length === 0) + throw new Error(`${definition.path}/members must be nonempty`); + const roles = new Map(); + let coordinatorAgent = false; + document.members.forEach((entry, position) => { + const member = object(entry, `${definition.path}/members/${position}`); + const role = text(member.role, `${definition.path}/members/${position}/role`); + if (roles.has(role)) + throw new Error(`${definition.path}/members/${position}/role duplicates ${role}`); + const declared = member.kind === 'agent' ? 'AgentDefinition' : member.kind === 'team' ? 'TeamDefinition' : null; + if (!declared) + throw new Error(`${definition.path}/members/${position}/kind must be agent or team`); + const pointer = `${definition.path}/members/${position}/definition`; + const target = resolve(index, member.definition, declared, pointer); + roles.set(role, declared); + edges.push({ key: target.key, pointer }); + if (role === document.coordinatorRole && declared === 'AgentDefinition') + coordinatorAgent = true; + }); + if (!coordinatorAgent) + throw new Error(`${definition.path}/coordinatorRole must name an agent member`); + if (!Array.isArray(document.communication)) + throw new Error(`${definition.path}/communication must be an array`); + document.communication.forEach((entry, position) => { + const edge = object(entry, `${definition.path}/communication/${position}`); + for (const field of ['fromRole', 'toRole']) + if (!roles.has(text(edge[field], `${definition.path}/communication/${position}/${field}`))) { + throw new Error(`${definition.path}/communication/${position}/${field} is not a team member role`); + } + }); + const acceptance = object(document.taskAcceptance, `${definition.path}/taskAcceptance`); + if (!Array.isArray(acceptance.allowedWorkflows)) + throw new Error(`${definition.path}/taskAcceptance/allowedWorkflows must be an array`); + acceptance.allowedWorkflows.forEach((entry, position) => { + const pointer = `${definition.path}/taskAcceptance/allowedWorkflows/${position}`; + const workflow = resolve(index, entry, 'WorkflowDefinition', pointer); + const steps = workflow.document.steps; + if (!Array.isArray(steps)) + throw new Error(`${workflow.path}/steps must be an array`); + steps.forEach((step, stepPosition) => { + const role = text(object(step, `${workflow.path}/steps/${stepPosition}`).role, `${workflow.path}/steps/${stepPosition}/role`); + if (!roles.has(role)) + throw new Error(`${workflow.path}/steps/${stepPosition}/role is not accepted by team ${document.id}`); + }); + }); + const limits = object(document.limits, `${definition.path}/limits`); + if (typeof limits.maxMembers === 'number' && document.members.length > limits.maxMembers) + throw new Error(`${definition.path}/members exceeds limits.maxMembers`); + } + if (definition.kind === 'WorkflowDefinition') { + if (!Array.isArray(document.steps) || document.steps.length === 0) + throw new Error(`${definition.path}/steps must be nonempty`); + const steps = new Map(); + document.steps.forEach((entry, position) => { + const step = object(entry, `${definition.path}/steps/${position}`); + const id = text(step.id, `${definition.path}/steps/${position}/id`); + if (steps.has(id)) + throw new Error(`${definition.path}/steps/${position}/id duplicates ${id}`); + if (!Array.isArray(step.dependsOn)) + throw new Error(`${definition.path}/steps/${position}/dependsOn must be an array`); + steps.set(id, step); + }); + const stepVisiting = new Set(), stepVisited = new Set(); + const visitStep = (id, trail) => { + if (stepVisiting.has(id)) + throw new Error(`${definition.path} workflow dependency cycle: ${[...trail, id].join(' -> ')}`); + if (stepVisited.has(id)) + return; + const step = steps.get(id); + if (!step) + throw new Error(`${definition.path} references unknown workflow dependency ${id}`); + stepVisiting.add(id); + for (const dependency of step.dependsOn) + visitStep(text(dependency, `${definition.path}/${id}/dependsOn`), [...trail, id]); + stepVisiting.delete(id); + stepVisited.add(id); + }; + for (const id of steps.keys()) + visitStep(id, []); + } + dependencies.set(definition.key, edges); + } + const visiting = new Set(), visited = new Set(); + const visit = (key, trail) => { + if (visiting.has(key)) + throw new Error(`Definition dependency cycle: ${[...trail, key.replace('\u0000', '@')].join(' -> ')}`); + if (visited.has(key)) + return; + visiting.add(key); + for (const edge of dependencies.get(key) ?? []) + visit(edge.key, [...trail, key.replace('\u0000', '@')]); + visiting.delete(key); + visited.add(key); + }; + visit(root.key, []); + for (const key of dependencies.keys()) + visit(key, []); + const rootLimits = object(root.document.limits, `${root.path}/limits`); + const maxDepth = Number(rootLimits.maxDepth), maxMembers = Number(rootLimits.maxMembers); + const reachable = new Set(); + let observedDepth = 0; + const measure = (key, depth) => { + observedDepth = Math.max(observedDepth, depth); + for (const edge of dependencies.get(key) ?? []) { + reachable.add(edge.key); + measure(edge.key, depth + 1); + } + }; + measure(root.key, 0); + if (Number.isSafeInteger(maxDepth) && observedDepth > maxDepth) + throw new Error(`${root.path} dependency depth ${observedDepth} exceeds limits.maxDepth ${maxDepth}`); + if (Number.isSafeInteger(maxMembers) && reachable.size > maxMembers) + throw new Error(`${root.path} dependency members ${reachable.size} exceeds limits.maxMembers ${maxMembers}`); +} diff --git a/skills/agent-team-creator/scripts/uar-package/maintenance.mjs b/skills/agent-team-creator/scripts/uar-package/maintenance.mjs new file mode 100644 index 0000000..fcbb13d --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/maintenance.mjs @@ -0,0 +1,43 @@ +import { object, text } from '../validation.mjs'; +import { flatten } from './canonical.mjs'; +import { compileUarPackage } from './compiler.mjs'; +import { loadUarPackage } from './package-files.mjs'; +function identity(document) { + return `${text(document.kind, 'kind')}:${text(document.id, 'id')}`; +} +function compare(before, after) { + const beforeId = text(before.manifest.id, 'before manifest id'), afterId = text(after.manifest.id, 'after manifest id'); + if (beforeId !== afterId) + throw new Error('Package maintenance cannot change package identity'); + const beforeDocuments = new Map(before.files.map(file => { + const document = object(JSON.parse(file.contentUtf8), file.path); + return [identity(document), { path: file.path, document }]; + })); + const afterDocuments = new Map(after.files.map(file => { + const document = object(JSON.parse(file.contentUtf8), file.path); + return [identity(document), { path: file.path, document }]; + })); + const added = [...afterDocuments.keys()].filter(key => !beforeDocuments.has(key)).sort(); + const removed = [...beforeDocuments.keys()].filter(key => !afterDocuments.has(key)).sort(); + const changed = []; + for (const key of [...beforeDocuments.keys()].filter(item => afterDocuments.has(item)).sort()) { + const leftDocument = beforeDocuments.get(key), rightDocument = afterDocuments.get(key); + const left = flatten(leftDocument.document), right = flatten(rightDocument.document); + const pointers = [...new Set([...left.keys(), ...right.keys()])].filter(pointer => left.get(pointer) !== right.get(pointer)).sort(); + if (pointers.length) + changed.push({ identity: key, beforePath: leftDocument.path, afterPath: rightDocument.path, pointers }); + } + const sameVersion = before.manifest.version === after.manifest.version; + if (sameVersion && before.manifest.contentDigest !== after.manifest.contentDigest) + throw new Error('Changed immutable package content requires a new semantic version'); + return { + packageId: beforeId, fromVersion: before.manifest.version, toVersion: after.manifest.version, + added, removed, changed, unchanged: before.manifest.contentDigest === after.manifest.contentDigest, + }; +} +export function diffUarPackages(beforeValue, afterValue) { + return compare(compileUarPackage(beforeValue), compileUarPackage(afterValue)); +} +export function diffUarDirectories(beforeDirectory, afterDirectory) { + return compare(loadUarPackage(beforeDirectory), loadUarPackage(afterDirectory)); +} diff --git a/skills/agent-team-creator/scripts/uar-package/migration.mjs b/skills/agent-team-creator/scripts/uar-package/migration.mjs new file mode 100644 index 0000000..014dd6b --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/migration.mjs @@ -0,0 +1,216 @@ +import { UAR_PROFILE_V1, UAR_PROFILE_V2 } from '../types.mjs'; +import { object, text, validateTeam } from '../validation.mjs'; +import { canonical, cloneObject, pointerSegment, selfDigest, sha256 } from './canonical.mjs'; +import { assertPortable } from './private-authority.mjs'; +const supportedCapabilities = new Set(['collaboration_definition_packages_v2']); +const contract = () => ({ type: 'object', additionalProperties: true }); +const limits = () => ({ concurrentTurns: 1, maxMembers: 16, maxDepth: 3, maxPendingTasks: 1000 }); +const budget = () => ({ maxTokens: 100000, maxCostMicrounits: 0, currency: 'USD', maxElapsedSeconds: 3600 }); +function safeId(value) { + const cleaned = value.normalize('NFC').toLowerCase().replace(/[^a-z0-9._/-]+/g, '-').replace(/^-+|-+$/g, ''); + return cleaned || 'imported'; +} +function diagnostic(sourceDocument, sourcePointer, disposition, reason, targetDocument, targetPointer) { + return { sourceDocument, sourcePointer, disposition, reason, ...(targetDocument ? { targetDocument } : {}), ...(targetPointer ? { targetPointer } : {}) }; +} +function leafDiagnostics(value, sourceDocument, targetDocument, disposition) { + const result = []; + const walk = (item, pointer) => { + if (item && typeof item === 'object') { + if (Array.isArray(item)) + item.forEach((child, index) => walk(child, `${pointer}/${index}`)); + else + Object.entries(item).forEach(([key, child]) => walk(child, `${pointer}/${pointerSegment(key)}`)); + return; + } + result.push(diagnostic(sourceDocument, pointer || '/', disposition, disposition === 'exact' ? 'field retained without semantic translation' : 'field retained through explicit profile translation', targetDocument, pointer || '/')); + }; + walk(value, ''); + return result; +} +function supportDiagnostics(document, sourceDocument) { + const result = []; + const extensions = object(document.extensions ?? {}, `${sourceDocument}.extensions`); + for (const [name, raw] of Object.entries(extensions)) { + const extension = object(raw, `${sourceDocument}.extensions.${name}`); + if (extension.required === true) + result.push(diagnostic(sourceDocument, `/extensions/${pointerSegment(name)}`, 'required-unsupported', `required extension ${name} has no mini execution adapter`)); + else + result.push(diagnostic(sourceDocument, `/extensions/${pointerSegment(name)}`, 'optional-unsupported', `optional extension ${name} is preserved but not executable`)); + } + if (Array.isArray(document.requiredCapabilities)) + document.requiredCapabilities.forEach((capability, index) => { + if (typeof capability === 'string' && !supportedCapabilities.has(capability)) + result.push(diagnostic(sourceDocument, `/requiredCapabilities/${index}`, 'required-unsupported', `required capability ${capability} is not implemented by mini`)); + }); + if (Array.isArray(document.capabilityDeclarations)) + document.capabilityDeclarations.forEach((raw, index) => { + const declaration = object(raw, `${sourceDocument}.capabilityDeclarations[${index}]`); + const capability = String(declaration.capability ?? ''); + if (!supportedCapabilities.has(capability)) + result.push(diagnostic(sourceDocument, `/capabilityDeclarations/${index}`, declaration.required === true ? 'required-unsupported' : 'optional-unsupported', `${declaration.required === true ? 'required' : 'optional'} capability ${capability} is not implemented by mini`)); + }); + return result; +} +function migrateDraft1Definition(source, diagnostics) { + const document = cloneObject(source.document, source.path); + const original = structuredClone(document); + document.profile = UAR_PROFILE_V2; + delete document.contentDigest; + diagnostics.push(...leafDiagnostics(original, source.path, source.path, 'exact').map(item => { + if (item.sourcePointer === '/profile') + return { ...item, disposition: 'translated', reason: 'draft.1 profile identifier translated to the accepted draft.2 profile' }; + if (item.sourcePointer === '/contentDigest') + return { ...item, disposition: 'translated', reason: 'source digest retained in source identity while draft.2 content receives a new digest' }; + return item; + })); + if (document.kind === 'AgentDefinition') { + const sourceDigest = typeof original.contentDigest === 'string' ? original.contentDigest : selfDigest(original); + document.sourceIdentity = { profile: UAR_PROFILE_V1, id: document.id, version: document.version, digest: sourceDigest, revision: null }; + document.renameMapping = { sourceId: document.id, targetId: document.id, reason: 'unchanged' }; + document.authoredFields = []; + for (const field of ['modelRequirements', 'promptDialect', 'ragConfiguration', 'contextStrategy', 'apiHarness']) + document[field] = { required: false, value: {} }; + document.legacySections = document.legacySections ?? {}; + document.sourceDescriptor = document.sourceDescriptor ?? original; + if (Array.isArray(document.skills)) + document.skills = document.skills.map(raw => { + const skill = cloneObject(raw, `${source.path}.skills`); + skill.entrypoint ??= null; + skill.requiredTools ??= []; + return skill; + }); + } + diagnostics.push(...supportDiagnostics(document, source.path)); + return { path: source.path, document }; +} +function migrateInline(value) { + const manifest = cloneObject(value.manifest, 'manifest'); + if (!Array.isArray(value.definitions)) + throw new Error('authoring package definitions must be an array'); + const sourceDefinitions = value.definitions.map((entry, index) => { + const item = object(entry, `definitions[${index}]`); + return { path: text(item.path, `definitions[${index}].path`), document: cloneObject(item.document, `definitions[${index}].document`) }; + }); + assertPortable({ manifest, definitions: sourceDefinitions }, 'inline-package'); + const sourceProfile = text(manifest.profile, 'manifest.profile'); + const diagnostics = []; + let definitions; + if (sourceProfile === UAR_PROFILE_V1) { + definitions = sourceDefinitions.map(item => migrateDraft1Definition(item, diagnostics)); + manifest.profile = UAR_PROFILE_V2; + manifest.requiredCapabilities = ['collaboration_definition_packages_v2']; + if (Array.isArray(manifest.capabilityDeclarations)) + manifest.capabilityDeclarations = [{ capability: 'collaboration_definition_packages_v2', required: true }]; + delete manifest.contentDigest; + delete manifest.files; + delete manifest.lock; + diagnostics.push(diagnostic('manifest.json', '/profile', 'translated', 'draft.1 manifest normalized to the accepted draft.2 profile', 'manifest.json', '/profile')); + } + else if (sourceProfile === UAR_PROFILE_V2) { + definitions = sourceDefinitions; + diagnostics.push(...leafDiagnostics({ manifest, definitions }, 'inline-package', 'workspace', 'exact')); + diagnostics.push(...supportDiagnostics(manifest, 'manifest.json')); + for (const item of definitions) + diagnostics.push(...supportDiagnostics(item.document, item.path)); + } + else + throw new Error(`Unsupported collaboration source profile: ${sourceProfile}`); + const receipt = { + schemaVersion: 1, sourceProfile, targetProfile: UAR_PROFILE_V2, diagnostics, + activationBlocked: diagnostics.some(item => item.disposition === 'required-unsupported'), + preservedSource: structuredClone(value), + }; + return { package: { manifest, definitions }, receipt }; +} +function skillReference(skill) { + const id = `urn:prometheus:skill:${safeId(skill)}`; + return { id, version: '0.0.0', digest: sha256(canonical(skill)), required: false, config: { legacyName: skill }, entrypoint: null, requiredTools: [] }; +} +function flatTeamPackage(value) { + const team = validateTeam(value); + assertPortable(team, 'flat-team'); + const version = '1.0.0'; + const base = `urn:prometheus:agent-team:${team.id}`; + const definitions = team.roles.map(role => { + const id = `${base}:agent:${role.id}`; + const source = role; + return { path: `agents/${role.id}.json`, document: { + profile: UAR_PROFILE_V2, kind: 'AgentDefinition', id, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, + requiredCapabilities: [], extensions: {}, title: role.id, role: role.id, + whenToUse: role.description, instructions: role.prompt, input: contract(), output: contract(), + skills: role.skills.map(skillReference), + models: [{ role: 'primary', capabilities: role.modelPolicy?.capabilities ?? [], preferredAliases: role.modelPolicy?.model ? [role.modelPolicy.model] : [] }], + permittedChildren: [], context: { mode: 'selected', artifacts: [], history: 'authorized-summary', memoryScopes: [] }, requestedLimits: limits(), + sourceIdentity: { profile: 'prometheus.agent-team/1', id: `${base}:legacy:${role.id}`, version, digest: sha256(canonical(source)), revision: null }, + renameMapping: { sourceId: `${base}:legacy:${role.id}`, targetId: id, reason: 'explicit-rename' }, authoredFields: [], + modelRequirements: { required: false, value: (role.modelPolicy ?? {}) }, promptDialect: { required: false, value: {} }, + ragConfiguration: { required: false, value: {} }, contextStrategy: { required: false, value: {} }, apiHarness: { required: false, value: { id: team.harness } }, + legacySections: {}, sourceDescriptor: structuredClone(role), + } }; + }); + const workflowId = `${base}:workflow:default`; + definitions.push({ path: 'workflows/default.json', document: { + profile: UAR_PROFILE_V2, kind: 'WorkflowDefinition', id: workflowId, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, requiredCapabilities: [], extensions: {}, + title: team.outcome, input: contract(), output: contract(), steps: team.roles.map(role => ({ + id: role.id, role: role.id, dependsOn: role.dependsOn, inputMapping: {}, output: contract(), effect: 'none', approval: 'none', + retry: { maxAttempts: 1, onUnknownEffect: 'reconcile-before-retry' }, completion: 'artifact', instructions: role.prompt, + })), failurePolicy: 'stop-dependent', maxActivations: 1000, + } }); + definitions.push({ path: 'teams/root.json', document: { + profile: UAR_PROFILE_V2, kind: 'TeamDefinition', id: `${base}:team`, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, requiredCapabilities: [], extensions: {}, + title: team.id, purpose: team.outcome, members: team.roles.map(role => ({ role: role.id, kind: 'agent', definition: { id: `${base}:agent:${role.id}`, version }, min: 1, max: 1, responsibility: role.description })), + coordinatorRole: team.roles[0].id, communication: [], taskAcceptance: { mode: 'operator', allowedWorkflows: [{ id: workflowId, version }] }, + routing: { eligibilityFirst: true, strategy: 'operator-role-capacity-cost-stable-id', explain: true }, limits: limits(), budget: budget(), input: contract(), output: contract(), + } }); + const manifest = { + profile: UAR_PROFILE_V2, kind: 'PackageManifest', id: `${base}:package`, version, + provenance: { source: 'prometheus portable flat team migration', authors: ['agent-team-creator'] }, + requiredCapabilities: ['collaboration_definition_packages_v2'], extensions: {}, entrypoints: [{ id: `${base}:team`, version }], + capabilityDeclarations: [{ capability: 'collaboration_definition_packages_v2', required: true }], resolution: 'exact-version-and-digest', + }; + const diagnostics = leafDiagnostics(team, 'flat-team.json', 'workspace', 'translated'); + return { package: { manifest, definitions }, receipt: { + schemaVersion: 1, sourceProfile: 'prometheus.agent-team/1', targetProfile: UAR_PROFILE_V2, + diagnostics, activationBlocked: false, preservedSource: structuredClone(team), + } }; +} +function agentArtifactPackage(value) { + const artifactId = safeId(text(value.id, 'AgentArtifact.id')); + const metadata = object(value.metadata ?? {}, 'AgentArtifact.metadata'); + const prefer = object(object(value.policy ?? {}, 'AgentArtifact.policy').skills ?? {}, 'AgentArtifact.policy.skills').prefer; + const skills = Array.isArray(prefer) ? prefer.filter((item) => typeof item === 'string') : []; + const team = { + schemaVersion: 1, id: artifactId, outcome: typeof metadata.description === 'string' ? metadata.description : `Imported ${artifactId}`, + scope: 'uar', harness: 'uar', roles: [{ id: 'agent', description: typeof metadata.description === 'string' ? metadata.description : artifactId, + prompt: typeof object(value.prompt ?? {}, 'AgentArtifact.prompt').system === 'string' ? String(object(value.prompt ?? {}).system) : `Operate as ${artifactId}.`, + skills, owns: [], inputs: [], outputs: [], dependsOn: [] }], + }; + const migrated = flatTeamPackage(team); + migrated.receipt.sourceProfile = 'uar.AgentArtifact/1'; + migrated.receipt.preservedSource = structuredClone(value); + migrated.receipt.diagnostics = leafDiagnostics(value, 'agent-artifact.json', 'workspace', 'translated'); + return migrated; +} +export function normalizeAuthoring(value) { + const source = object(value, 'authoring input'); + if (source.package !== undefined) + return normalizeAuthoring(source.package); + if (source.manifest !== undefined && source.definitions !== undefined) + return migrateInline(source); + if (source.schemaVersion === 1 && Array.isArray(source.roles)) + return flatTeamPackage(source); + if (source.kind === 'agent' && source.runtime !== undefined) { + assertPortable(source, 'agent-artifact'); + return agentArtifactPackage(source); + } + throw new Error('Unsupported authoring input; expected workspace, inline package, flat team, or legacy AgentArtifact'); +} +export function assertMigrationAllowsBuild(receipt) { + const blocked = receipt?.diagnostics.find(item => item.disposition === 'required-unsupported'); + if (blocked) + throw new Error(`${blocked.sourceDocument}${blocked.sourcePointer}: required migration semantics are unsupported (${blocked.reason})`); +} diff --git a/skills/agent-team-creator/scripts/uar-package/package-files.mjs b/skills/agent-team-creator/scripts/uar-package/package-files.mjs new file mode 100644 index 0000000..31bc94f --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/package-files.mjs @@ -0,0 +1,71 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { object, relativeFile, text } from '../validation.mjs'; +import { selfDigest, sha256 } from './canonical.mjs'; +import { compileUarPackage } from './compiler.mjs'; +function writeExclusive(file, content) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + const descriptor = fs.openSync(file, 'wx', 0o600); + try { + fs.writeFileSync(descriptor, content); + fs.fsyncSync(descriptor); + } + finally { + fs.closeSync(descriptor); + } +} +export function writeUarPackage(directoryValue, value, receipt) { + const compiled = compileUarPackage(value, receipt); + const directory = path.resolve(text(directoryValue, 'out')); + if (fs.existsSync(directory)) + throw new Error(`Immutable package destination already exists: ${directory}`); + fs.mkdirSync(path.dirname(directory), { recursive: true }); + const temporary = path.join(path.dirname(directory), `.${path.basename(directory)}.${randomUUID()}.tmp`); + fs.mkdirSync(temporary); + try { + for (const file of compiled.files) + writeExclusive(path.join(temporary, ...file.path.split('/')), file.contentUtf8); + writeExclusive(path.join(temporary, 'manifest.json'), compiled.manifestUtf8); + fs.renameSync(temporary, directory); + } + finally { + fs.rmSync(temporary, { recursive: true, force: true }); + } + return { directory, manifest: compiled.manifest, files: ['manifest.json', ...compiled.files.map(file => file.path)] }; +} +export function loadUarPackage(directoryValue) { + const directory = path.resolve(text(directoryValue, 'packageDirectory')); + const manifestUtf8 = fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'); + const manifest = object(JSON.parse(manifestUtf8), 'manifest'); + if (!Array.isArray(manifest.files)) + throw new Error('Compiled package manifest.files must be an array'); + const files = manifest.files.map((entry, index) => { + const item = object(entry, `manifest.files[${index}]`); + const file = relativeFile(text(item.path, `manifest.files[${index}].path`)); + const contentUtf8 = fs.readFileSync(path.join(directory, ...file.split('/')), 'utf8'); + if (sha256(contentUtf8) !== item.byteDigest) + throw new Error(`Package byte digest mismatch: ${file}`); + const document = object(JSON.parse(contentUtf8), file); + if (selfDigest(document) !== document.contentDigest) + throw new Error(`Package content digest mismatch: ${file}`); + const reference = object(item.definition, `manifest.files[${index}].definition`); + for (const field of ['id', 'version']) + if (document[field] !== reference[field]) + throw new Error(`Package file identity mismatch: ${file} ${field}`); + if (document.contentDigest !== reference.digest) + throw new Error(`Package file identity mismatch: ${file} digest`); + if (document.kind !== item.kind) + throw new Error(`Package file kind mismatch: ${file}`); + return { path: file, contentUtf8 }; + }); + if (selfDigest(manifest) !== manifest.contentDigest) + throw new Error('Package manifest content digest mismatch'); + return { manifest, manifestUtf8, files }; +} +export function compiledAsAuthoring(compiled) { + const manifest = structuredClone(compiled.manifest); + delete manifest.files; + delete manifest.lock; + return { manifest, definitions: compiled.files.map(file => ({ path: file.path, document: object(JSON.parse(file.contentUtf8), file.path) })) }; +} diff --git a/skills/agent-team-creator/scripts/uar-package/private-authority.mjs b/skills/agent-team-creator/scripts/uar-package/private-authority.mjs new file mode 100644 index 0000000..a7da99e --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/private-authority.mjs @@ -0,0 +1,70 @@ +import { pointerSegment } from './canonical.mjs'; +const privateKinds = new Set(['DeploymentBinding', 'RepresentationGrant', 'EffectiveBindingReceipt']); +const forbiddenKeys = /^(?:credential|credentialValue|token|secret|password|apiKey|privateKey|consentEvidence(?:Ref)?|representationGrant(?:Ref|Refs)?|effectiveBindingReceiptRef|installedAuthority)$/i; +const recognizedSecret = /(?:^|\b)(?:token|secret|password|api[-_ ]?key)\s*(?:=|:|is)?\s*[A-Za-z0-9_./+:-]{16,}/i; +export function privateAuthorityDiagnostics(value, sourceDocument) { + const diagnostics = []; + const walk = (item, pointer) => { + if (typeof item === 'string' && recognizedSecret.test(item)) + diagnostics.push({ + sourceDocument, sourcePointer: pointer || '/', disposition: 'required-unsupported', + reason: 'recognized credential or secret material is forbidden in portable content', + }); + if (Array.isArray(item)) + return item.forEach((child, index) => walk(child, `${pointer}/${index}`)); + if (!item || typeof item !== 'object') + return; + if (typeof item.kind === 'string' && privateKinds.has(item.kind)) + diagnostics.push({ + sourceDocument, sourcePointer: `${pointer}/kind`, disposition: 'required-unsupported', + reason: `${item.kind} is private installed state and cannot enter a portable package`, + }); + for (const [key, child] of Object.entries(item)) { + const childPointer = `${pointer}/${pointerSegment(key)}`; + if (forbiddenKeys.test(key)) + diagnostics.push({ + sourceDocument, sourcePointer: childPointer, disposition: 'required-unsupported', + reason: 'private credential, consent, or authority values are forbidden in portable content', + }); + else if (key === 'credentialRef' && sourceDocument !== 'deployment-binding') + diagnostics.push({ + sourceDocument, sourcePointer: childPointer, disposition: 'required-unsupported', + reason: 'credential references belong only in a private deployment binding', + }); + walk(child, childPointer); + } + }; + walk(value, ''); + return diagnostics; +} +export function assertPortable(value, sourceDocument) { + const diagnostics = privateAuthorityDiagnostics(value, sourceDocument); + if (diagnostics.length) + throw new Error(`${diagnostics[0].sourceDocument}${diagnostics[0].sourcePointer}: ${diagnostics[0].reason}`); +} +export function assertOpaqueReference(value, pointer) { + if (typeof value !== 'string' || !value.trim()) + throw new Error(`${pointer} must be a nonempty opaque reference`); + if (/^(?:bearer|token|secret|password|api[-_]?key):/i.test(value) || recognizedSecret.test(value)) { + throw new Error(`${pointer} must be an opaque host reference, not a credential value`); + } + return value; +} +export function assertBindingContainsReferencesOnly(value) { + const walk = (item, pointer) => { + if (typeof item === 'string' && recognizedSecret.test(item)) + throw new Error(`${pointer || '/'} contains recognized credential material`); + if (Array.isArray(item)) + return item.forEach((child, index) => walk(child, `${pointer}/${index}`)); + if (!item || typeof item !== 'object') + return; + for (const [key, child] of Object.entries(item)) { + const childPointer = `${pointer}/${pointerSegment(key)}`; + if (/^(?:credential|credentialValue|token|secret|password|apiKey|privateKey|consentEvidence|installedAuthority)$/i.test(key)) { + throw new Error(`${childPointer} contains a private value; use an opaque reference field`); + } + walk(child, childPointer); + } + }; + walk(value, ''); +} diff --git a/skills/agent-team-creator/scripts/uar-package/profile-validation.mjs b/skills/agent-team-creator/scripts/uar-package/profile-validation.mjs new file mode 100644 index 0000000..705c65a --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/profile-validation.mjs @@ -0,0 +1,172 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { canonical, pointerSegment } from './canonical.mjs'; +const schemaDirectory = fileURLToPath(new URL('../../schemas/uar/0.1.0-draft.2/', import.meta.url)); +const schemaFiles = { + AgentDefinition: 'agent-definition.schema.json', + TeamDefinition: 'team-definition.schema.json', + WorkflowDefinition: 'workflow-definition.schema.json', + PackageManifest: 'package-manifest.schema.json', + DeploymentBinding: 'deployment-binding.schema.json', + ConversionReport: 'conversion-report.schema.json', +}; +const cache = new Map(); +function schema(file) { + const found = cache.get(file); + if (found) + return found; + const value = JSON.parse(readFileSync(new URL(`../../schemas/uar/0.1.0-draft.2/${file}`, import.meta.url), 'utf8')); + cache.set(file, value); + return value; +} +function resolvePointer(root, fragment) { + let current = root; + if (!fragment || fragment === '#') + return root; + if (!fragment.startsWith('#/')) + throw new Error(`Unsupported schema reference fragment: ${fragment}`); + for (const part of fragment.slice(2).split('/')) { + const key = part.replaceAll('~1', '/').replaceAll('~0', '~'); + if (!current || typeof current !== 'object' || !(key in current)) + throw new Error(`Unresolved schema reference: ${fragment}`); + current = current[key]; + } + if (!current || typeof current !== 'object' || Array.isArray(current)) + throw new Error(`Schema reference is not an object: ${fragment}`); + return current; +} +function resolveReference(reference, currentFile) { + const [filePart, fragmentPart] = reference.split('#', 2); + const file = filePart || currentFile; + return { file, value: resolvePointer(schema(file), fragmentPart === undefined ? '#' : `#${fragmentPart}`) }; +} +function typeMatches(value, type) { + if (type === 'null') + return value === null; + if (type === 'array') + return Array.isArray(value); + if (type === 'object') + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); + if (type === 'integer') + return Number.isSafeInteger(value); + if (type === 'number') + return typeof value === 'number' && Number.isFinite(value); + return typeof value === type; +} +function failure(pointer, message) { + throw new Error(`Schema validation failed at ${pointer || '/'}: ${message}`); +} +function validateNode(value, rule, file, pointer) { + if (typeof rule.$ref === 'string') { + const resolved = resolveReference(rule.$ref, file); + validateNode(value, resolved.value, resolved.file, pointer); + return; + } + if (Array.isArray(rule.oneOf)) { + const matches = rule.oneOf.filter(candidate => { + try { + validateNode(value, candidate, file, pointer); + return true; + } + catch { + return false; + } + }); + if (matches.length !== 1) + failure(pointer, `expected exactly one schema branch, observed ${matches.length}`); + return; + } + if ('const' in rule && canonical(value) !== canonical(rule.const)) + failure(pointer, `must equal ${JSON.stringify(rule.const)}`); + if (Array.isArray(rule.enum) && !rule.enum.some(item => canonical(item) === canonical(value))) + failure(pointer, 'value is outside the allowed enumeration'); + if (rule.type !== undefined) { + const types = Array.isArray(rule.type) ? rule.type : [rule.type]; + if (!types.some(type => typeof type === 'string' && typeMatches(value, type))) + failure(pointer, `expected type ${types.join(' or ')}`); + } + if (typeof value === 'string') { + if (typeof rule.minLength === 'number' && value.length < rule.minLength) + failure(pointer, `must contain at least ${rule.minLength} characters`); + if (typeof rule.maxLength === 'number' && [...value].length > rule.maxLength) + failure(pointer, `must contain at most ${rule.maxLength} characters`); + if (typeof rule.pattern === 'string' && !new RegExp(rule.pattern, 'u').test(value)) + failure(pointer, `does not match ${rule.pattern}`); + if (rule.format === 'uri') { + try { + new URL(value); + } + catch { + failure(pointer, 'must be a URI'); + } + } + if (rule.format === 'uri-reference') { + try { + new URL(value, 'https://schemas.prometheus-ags.dev/'); + } + catch { + failure(pointer, 'must be a URI reference'); + } + } + if (rule.format === 'date-time' && !Number.isFinite(Date.parse(value))) + failure(pointer, 'must be an RFC 3339 date-time'); + } + if (typeof value === 'number') { + if (typeof rule.minimum === 'number' && value < rule.minimum) + failure(pointer, `must be at least ${rule.minimum}`); + if (typeof rule.maximum === 'number' && value > rule.maximum) + failure(pointer, `must be at most ${rule.maximum}`); + } + if (Array.isArray(value)) { + if (typeof rule.minItems === 'number' && value.length < rule.minItems) + failure(pointer, `must contain at least ${rule.minItems} items`); + if (typeof rule.maxItems === 'number' && value.length > rule.maxItems) + failure(pointer, `must contain at most ${rule.maxItems} items`); + if (rule.uniqueItems === true) { + const keys = value.map(item => canonical(item)); + if (new Set(keys).size !== keys.length) + failure(pointer, 'items must be unique'); + } + if (rule.items && typeof rule.items === 'object') + value.forEach((item, index) => validateNode(item, rule.items, file, `${pointer}/${index}`)); + } + if (value && typeof value === 'object' && !Array.isArray(value)) { + const record = value; + const properties = rule.properties && typeof rule.properties === 'object' ? rule.properties : {}; + if (Array.isArray(rule.required)) + for (const key of rule.required) { + if (typeof key === 'string' && !(key in record)) + failure(`${pointer}/${pointerSegment(key)}`, 'required property is missing'); + } + for (const [key, child] of Object.entries(record)) { + const childPointer = `${pointer}/${pointerSegment(key)}`; + if (properties[key]) + validateNode(child, properties[key], file, childPointer); + else if (rule.additionalProperties === false) + failure(childPointer, 'additional property is forbidden'); + else if (rule.additionalProperties && typeof rule.additionalProperties === 'object') + validateNode(child, rule.additionalProperties, file, childPointer); + if (rule.propertyNames && typeof rule.propertyNames === 'object') + validateNode(key, rule.propertyNames, file, childPointer); + } + } +} +export function validateProfileDocument(value) { + if (value.profile !== UAR_PROFILE_V2) + throw new Error(`profile must be ${UAR_PROFILE_V2}`); + const kind = String(value.kind ?? ''); + const file = schemaFiles[kind]; + if (!file) + throw new Error(`Unsupported draft.2 document kind: ${kind}`); + validateNode(value, schema(file), file, ''); +} +export function profileSchemaInfo() { + const receipt = JSON.parse(readFileSync(new URL('../../schemas/uar/0.1.0-draft.2/consumer-source-receipt.json', import.meta.url), 'utf8')); + return { + profile: UAR_PROFILE_V2, + sourceRevision: receipt.provider.commit, + directory: schemaDirectory, + documents: structuredClone(schemaFiles), + }; +} diff --git a/skills/agent-team-creator/scripts/uar-package/workspace-questions.mjs b/skills/agent-team-creator/scripts/uar-package/workspace-questions.mjs new file mode 100644 index 0000000..64d668d --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/workspace-questions.mjs @@ -0,0 +1,72 @@ +import { object } from '../validation.mjs'; +function decode(segment) { + return segment.replaceAll('~1', '/').replaceAll('~0', '~'); +} +export function pointerValue(document, pointer) { + if (pointer === '') + return document; + let current = document; + for (const raw of pointer.split('/').slice(1)) { + if (current === null || typeof current !== 'object') + return undefined; + const key = decode(raw); + current = Array.isArray(current) ? current[Number(key)] : current[key]; + } + return current; +} +export function setPointer(document, pointer, value) { + const segments = pointer.split('/').slice(1).map(decode); + if (!segments.length) + throw new Error('A guided answer cannot replace a complete document'); + let current = document; + for (const segment of segments.slice(0, -1)) { + const existing = current[segment]; + if (existing === undefined) + current[segment] = {}; + current = object(current[segment], `question target ${pointer}`); + } + current[segments.at(-1)] = structuredClone(value); +} +function seed(id, document, pointer, prompt) { + return { id, document, pointer, prompt, required: true }; +} +function seeds(manifestPath, definitions) { + const result = [ + seed('manifest.provenance', manifestPath, '/provenance', 'What source and authors establish package provenance?'), + seed('manifest.entrypoint', manifestPath, '/entrypoints', 'Which single TeamDefinition is the top-level package entrypoint?'), + ]; + const teams = definitions.filter(item => item.document.kind === 'TeamDefinition'); + const agents = definitions.filter(item => item.document.kind === 'AgentDefinition'); + const workflows = definitions.filter(item => item.document.kind === 'WorkflowDefinition'); + for (const definition of teams) { + const prefix = `team.${definition.document.id}`; + result.push(seed(`${prefix}.members`, definition.path, '/members', `Which agents or nested teams belong to ${definition.document.id}?`), seed(`${prefix}.coordinator`, definition.path, '/coordinatorRole', `Which agent role coordinates ${definition.document.id}?`), seed(`${prefix}.communication`, definition.path, '/communication', `Which explicit role communication edges are allowed for ${definition.document.id}?`), seed(`${prefix}.acceptance`, definition.path, '/taskAcceptance/allowedWorkflows', `Which exact workflows may ${definition.document.id} accept?`), seed(`${prefix}.limits`, definition.path, '/limits', `Which aggregate limits constrain ${definition.document.id}?`), seed(`${prefix}.budget`, definition.path, '/budget', `Which aggregate budget constrains ${definition.document.id}?`)); + } + for (const definition of agents) { + const prefix = `agent.${definition.document.id}`; + result.push(seed(`${prefix}.children`, definition.path, '/permittedChildren', `Which exact child agents may ${definition.document.id} invoke?`), seed(`${prefix}.skills`, definition.path, '/skills', `Which exact skill locks does ${definition.document.id} require?`), seed(`${prefix}.models`, definition.path, '/models', `Which model capabilities and aliases does ${definition.document.id} request?`), seed(`${prefix}.context`, definition.path, '/context', `Which bounded context may ${definition.document.id} receive?`), seed(`${prefix}.limits`, definition.path, '/requestedLimits', `Which limits constrain ${definition.document.id}?`)); + } + for (const definition of workflows) + result.push(seed(`workflow.${definition.document.id}.steps`, definition.path, '/steps', `Which finite role steps define ${definition.document.id}?`)); + result.push(seed('workspace.binding-intent', 'workspace.json', '/bindingIntent', 'Should deployment stop after package installation or prepare a private binding?')); + return result; +} +function documentMap(manifestPath, manifest, definitions, bindingIntent) { + return new Map([ + [manifestPath, manifest], + ...definitions.map(item => [item.path, item.document]), + ['workspace.json', { bindingIntent: bindingIntent ?? null }], + ]); +} +export function questionState(manifestPath, manifest, definitions, bindingIntent, previous) { + const documents = documentMap(manifestPath, manifest, definitions, bindingIntent); + const prior = new Map(previous?.questions.map(question => [question.id, question]) ?? []); + const questions = seeds(manifestPath, definitions).map((item) => { + const value = pointerValue(documents.get(item.document), item.pointer); + const accepted = prior.get(item.id)?.answer; + const answer = value === undefined || value === null ? accepted : value; + return { ...item, state: answer === undefined || answer === null ? 'pending' : 'answered', ...(answer === undefined || answer === null ? {} : { answer: structuredClone(answer) }) }; + }); + const currentQuestionId = questions.find(item => item.required && item.state === 'pending')?.id ?? null; + return { currentQuestionId, questions }; +} diff --git a/skills/agent-team-creator/scripts/uar-package/workspace.mjs b/skills/agent-team-creator/scripts/uar-package/workspace.mjs new file mode 100644 index 0000000..095343b --- /dev/null +++ b/skills/agent-team-creator/scripts/uar-package/workspace.mjs @@ -0,0 +1,391 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { UAR_PROFILE_V2 } from '../types.mjs'; +import { id, object, relativeFile, text } from '../validation.mjs'; +import { commitChanges, projectFile, readFile, stage } from '../project-files.mjs'; +import { SEMVER } from './canonical.mjs'; +import { compileUarPackage } from './compiler.mjs'; +import { normalizeAuthoring } from './migration.mjs'; +import { compiledAsAuthoring, loadUarPackage } from './package-files.mjs'; +import { questionState, setPointer } from './workspace-questions.mjs'; +const INDEX_FILE = 'workspace.json'; +const DEFAULT_PAGE_SIZE = 20; +const MAX_PAGE_SIZE = 50; +function projectRoot(input) { + return fs.realpathSync(path.resolve(text(input.project, 'project'))); +} +function teamId(input, field = 'workspace') { + return id(input[field], field); +} +function workspacePath(input, field = 'workspace') { + return `.agent-team/${teamId(input, field)}/authoring`; +} +function expectedRevision(input) { + if (!Number.isSafeInteger(input.expectedRevision) || Number(input.expectedRevision) < 0) + throw new Error('expectedRevision must be a nonnegative integer from the current workspace'); + return Number(input.expectedRevision); +} +function json(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} +function readJson(file, label) { + return object(JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')), label); +} +function validateQuestionState(value) { + const state = object(value, 'workspace.questionState'); + if (!Array.isArray(state.questions)) + throw new Error('workspace.questionState.questions must be an array'); + const ids = new Set(); + for (const raw of state.questions) { + const question = object(raw, 'workspace question'), questionId = text(question.id, 'workspace question id'); + if (ids.has(questionId)) + throw new Error(`Duplicate workspace question id: ${questionId}`); + ids.add(questionId); + relativeFile(text(question.document, 'workspace question document')); + if (typeof question.pointer !== 'string' || !question.pointer.startsWith('/')) + throw new Error('workspace question pointer must be a JSON Pointer'); + text(question.prompt, 'workspace question prompt'); + if (question.required !== true) + throw new Error('workspace questions must be mandatory'); + if (!['pending', 'answered'].includes(String(question.state))) + throw new Error('workspace question state must be pending or answered'); + if (question.state === 'answered' && question.answer === undefined) + throw new Error(`Answered workspace question lacks an answer: ${questionId}`); + } + if (state.currentQuestionId !== null && (typeof state.currentQuestionId !== 'string' || !ids.has(state.currentQuestionId))) + throw new Error('workspace.currentQuestionId must name a persisted question or be null'); + return structuredClone(state); +} +function validateIndex(value) { + const index = object(value, 'workspace index'); + const allowed = new Set(['schemaVersion', 'revision', 'profile', 'teamId', 'packageId', 'packageVersion', 'manifest', 'definitions', 'migrationReceipt', 'bindingIntent', 'base', 'questionState']); + for (const field of Object.keys(index)) + if (!allowed.has(field)) + throw new Error(`Unknown workspace index field: ${field}`); + if (index.schemaVersion !== 1) + throw new Error('workspace.schemaVersion must be 1'); + if (!Number.isSafeInteger(index.revision) || Number(index.revision) < 1) + throw new Error('workspace.revision must be a positive integer'); + if (index.profile !== UAR_PROFILE_V2) + throw new Error(`workspace.profile must be ${UAR_PROFILE_V2}`); + id(index.teamId, 'workspace.teamId'); + text(index.packageId, 'workspace.packageId'); + if (!SEMVER.test(text(index.packageVersion, 'workspace.packageVersion'))) + throw new Error('workspace.packageVersion must be semantic version x.y.z'); + relativeFile(text(index.manifest, 'workspace.manifest')); + if (!Array.isArray(index.definitions) || index.definitions.length === 0) + throw new Error('workspace.definitions must be a nonempty array'); + const seen = new Set(); + for (const [position, item] of index.definitions.entries()) { + const file = relativeFile(text(item, `workspace.definitions[${position}]`)), folded = file.normalize('NFC').toLocaleLowerCase('en-US'); + if (seen.has(folded)) + throw new Error(`Case-insensitive workspace path collision: ${file}`); + seen.add(folded); + } + const manifestFolded = String(index.manifest).normalize('NFC').toLocaleLowerCase('en-US'); + if (seen.has(manifestFolded) || manifestFolded === INDEX_FILE) + throw new Error('Workspace manifest path collides with another source document'); + if (index.migrationReceipt !== undefined) + relativeFile(text(index.migrationReceipt, 'workspace.migrationReceipt')); + if (index.bindingIntent !== undefined && !['package-only', 'package-and-binding'].includes(String(index.bindingIntent))) + throw new Error('Invalid workspace.bindingIntent'); + if (index.base !== undefined) { + const base = object(index.base, 'workspace.base'); + id(base.teamId, 'workspace.base.teamId'); + if (!Number.isSafeInteger(base.revision) || Number(base.revision) < 1) + throw new Error('workspace.base.revision must be positive'); + if (!SEMVER.test(text(base.packageVersion, 'workspace.base.packageVersion'))) + throw new Error('workspace.base.packageVersion must be semantic'); + } + validateQuestionState(index.questionState); + return structuredClone(index); +} +function lock(project, workspace) { + const lockFile = projectFile(project, `${workspace}.lock`); + fs.mkdirSync(path.dirname(lockFile), { recursive: true }); + const token = randomUUID(); + let descriptor; + try { + descriptor = fs.openSync(lockFile, 'wx', 0o600); + } + catch (error) { + if (error.code === 'EEXIST') + throw new Error(`Workspace lock held: ${workspace}.lock; inspect it before manual recovery`); + throw error; + } + try { + fs.writeFileSync(descriptor, JSON.stringify({ token, pid: process.pid, at: new Date().toISOString() })); + fs.fsyncSync(descriptor); + } + catch (error) { + fs.closeSync(descriptor); + fs.rmSync(lockFile, { force: true }); + throw error; + } + fs.closeSync(descriptor); + return () => { try { + if (JSON.parse(fs.readFileSync(lockFile, 'utf8')).token === token) + fs.rmSync(lockFile); + } + catch { /* replaced lock belongs to another writer */ } }; +} +export function loadWorkspace(input) { + const project = projectRoot(input), workspace = workspacePath(input), base = `${workspace}/${INDEX_FILE}`; + const index = validateIndex(readJson(projectFile(project, base), base)); + if (index.teamId !== teamId(input)) + throw new Error('Workspace team identity does not match its project path'); + const manifestPath = `${workspace}/${index.manifest}`, manifest = readJson(projectFile(project, manifestPath), manifestPath); + if (manifest.id !== index.packageId || manifest.version !== index.packageVersion) + throw new Error('Workspace index package identity/version does not match its manifest source'); + const definitions = index.definitions.map(file => ({ path: file, document: readJson(projectFile(project, `${workspace}/${file}`), `${workspace}/${file}`) })); + let migrationReceipt; + if (index.migrationReceipt) + migrationReceipt = readJson(projectFile(project, `${workspace}/${index.migrationReceipt}`), 'migration receipt'); + return { root: workspace, index, package: { manifest, definitions }, ...(migrationReceipt ? { migrationReceipt } : {}) }; +} +function assertRevision(workspace, expected) { + if (workspace.index.revision !== expected) + throw new Error(`Stale workspace revision: expected ${expected}, current ${workspace.index.revision}; reload before writing`); +} +export function initializeWorkspace(input) { + const project = projectRoot(input), workspaceId = teamId(input), workspace = workspacePath(input); + if (expectedRevision(input) !== 0) + throw new Error('New workspace expectedRevision must be 0'); + const release = lock(project, workspace); + try { + const indexFile = projectFile(project, `${workspace}/${INDEX_FILE}`); + if (fs.existsSync(indexFile)) + throw new Error(`Workspace already exists: ${workspaceId}`); + const migrationSource = input.sourceDirectory === undefined ? input.source : compiledAsAuthoring(loadUarPackage(projectFile(project, relativeFile(text(input.sourceDirectory, 'sourceDirectory'))))); + const normalized = migrationSource === undefined ? null : normalizeAuthoring(migrationSource); + const packageId = normalized ? text(normalized.package.manifest.id, 'manifest.id') : text(input.packageId, 'packageId'); + const packageVersion = normalized ? text(normalized.package.manifest.version, 'manifest.version') : text(input.packageVersion, 'packageVersion'); + if (!SEMVER.test(packageVersion)) + throw new Error('packageVersion must be semantic version x.y.z'); + const definitionPaths = normalized ? normalized.package.definitions.map(item => item.path) : (() => { + if (!Array.isArray(input.definitionPaths) || input.definitionPaths.length === 0) + throw new Error('definitionPaths must declare at least one source document path'); + return input.definitionPaths.map((item, position) => relativeFile(text(item, `definitionPaths[${position}]`))); + })(); + const manifestPath = input.manifestPath === undefined ? 'manifest.source.json' : relativeFile(text(input.manifestPath, 'manifestPath')); + const manifest = normalized ? normalized.package.manifest : { profile: UAR_PROFILE_V2, kind: 'PackageManifest', id: packageId, version: packageVersion }; + const definitions = normalized ? normalized.package.definitions : definitionPaths.map(file => ({ path: file, document: {} })); + const migrationReceipt = normalized?.receipt ?? { schemaVersion: 1, sourceProfile: 'new-authoring', targetProfile: UAR_PROFILE_V2, diagnostics: [], activationBlocked: false }; + const bindingIntent = input.bindingIntent ?? 'package-only'; + const index = validateIndex({ schemaVersion: 1, revision: 1, profile: UAR_PROFILE_V2, teamId: workspaceId, packageId, packageVersion, manifest: manifestPath, definitions: definitionPaths, migrationReceipt: 'migration-receipt.json', bindingIntent, questionState: questionState(manifestPath, manifest, definitions, String(bindingIntent)) }); + const changes = new Map(); + stage(changes, project, `${workspace}/${INDEX_FILE}`, json(index)); + stage(changes, project, `${workspace}/${index.manifest}`, json(manifest)); + for (const definition of definitions) + stage(changes, project, `${workspace}/${definition.path}`, json(definition.document)); + stage(changes, project, `${workspace}/${index.migrationReceipt}`, json(migrationReceipt)); + const recovery = commitChanges(project, [...changes.values()], { operation: 'workspace-init', teamId: workspaceId, beforeRevision: 0, afterRevision: 1 }); + return { workspace: workspaceId, path: workspace, revision: 1, packageId: index.packageId, packageVersion: index.packageVersion, documents: definitions.length + 2, recovery }; + } + finally { + release(); + } +} +function stageMutation(project, workspace, operation, changes) { + const beforeRevision = workspace.index.revision, afterRevision = beforeRevision + 1; + workspace.index.revision = afterRevision; + stage(changes, project, `${workspace.root}/${INDEX_FILE}`, json(workspace.index)); + const recovery = commitChanges(project, [...changes.values()], { operation, teamId: workspace.index.teamId, beforeRevision, afterRevision }); + return { workspace: workspace.index.teamId, path: workspace.root, beforeRevision, revision: afterRevision, recovery }; +} +export function updateWorkspaceDocument(input) { + const project = projectRoot(input), workspace = workspacePath(input), file = relativeFile(text(input.path, 'path')), release = lock(project, workspace); + try { + const current = loadWorkspace(input); + assertRevision(current, expectedRevision(input)); + const allowed = new Set([current.index.manifest, ...current.index.definitions]); + if (!allowed.has(file)) + throw new Error(`Workspace update path is not declared by workspace.json: ${file}`); + const document = object(input.document, 'document'); + if (file === current.index.manifest) { + if (document.kind !== 'PackageManifest' || document.id !== current.index.packageId || document.version !== current.index.packageVersion) + throw new Error('Manifest update cannot change package kind, identity, or version'); + current.package.manifest = structuredClone(document); + } + else { + if (!['AgentDefinition', 'TeamDefinition', 'WorkflowDefinition'].includes(String(document.kind))) + throw new Error('Definition update must contain one portable collaboration definition'); + const selected = current.package.definitions.find(item => item.path === file); + if (selected.document.kind !== document.kind || selected.document.id !== document.id || selected.document.version !== document.version) + throw new Error('Document update cannot change immutable kind, identity, or version; use workspace revision'); + selected.document = structuredClone(document); + } + current.index.questionState = questionState(current.index.manifest, current.package.manifest, current.package.definitions, current.index.bindingIntent, current.index.questionState); + const changes = new Map(); + stage(changes, project, `${workspace}/${file}`, json(document)); + return { ...stageMutation(project, current, 'workspace-update', changes), path: file, kind: document.kind, id: document.id, version: document.version }; + } + finally { + release(); + } +} +export function answerWorkspaceQuestion(input) { + const project = projectRoot(input), workspace = workspacePath(input), release = lock(project, workspace); + try { + const current = loadWorkspace(input); + assertRevision(current, expectedRevision(input)); + const questionId = text(input.questionId, 'questionId'), question = current.index.questionState.questions.find(item => item.id === questionId); + if (!question) + throw new Error(`Unknown workspace question: ${questionId}`); + if (input.answer === undefined) + throw new Error('answer is required'); + const changes = new Map(); + if (question.document === INDEX_FILE) { + if (question.pointer !== '/bindingIntent' || !['package-only', 'package-and-binding'].includes(String(input.answer))) + throw new Error('Invalid binding-intent answer'); + current.index.bindingIntent = input.answer; + } + else { + const selected = question.document === current.index.manifest ? current.package.manifest : current.package.definitions.find(item => item.path === question.document)?.document; + if (!selected) + throw new Error(`Question document is no longer declared: ${question.document}`); + setPointer(selected, question.pointer, input.answer); + stage(changes, project, `${current.root}/${question.document}`, json(selected)); + } + current.index.questionState = questionState(current.index.manifest, current.package.manifest, current.package.definitions, current.index.bindingIntent, current.index.questionState); + return { ...stageMutation(project, current, 'workspace-answer', changes), questionId, currentQuestionId: current.index.questionState.currentQuestionId }; + } + finally { + release(); + } +} +export function workspaceStatus(input) { + const workspace = loadWorkspace(input), diagnostics = workspace.migrationReceipt?.diagnostics ?? []; + const cursor = input.cursor === undefined ? 0 : Number(input.cursor), requested = input.pageSize === undefined ? DEFAULT_PAGE_SIZE : Number(input.pageSize); + if (!Number.isSafeInteger(cursor) || cursor < 0) + throw new Error('cursor must be a nonnegative integer'); + if (!Number.isSafeInteger(requested) || requested < 1 || requested > MAX_PAGE_SIZE) + throw new Error(`pageSize must be between 1 and ${MAX_PAGE_SIZE}`); + const items = diagnostics.slice(cursor, cursor + requested), next = cursor + items.length; + const current = workspace.index.questionState.questions.find(item => item.id === workspace.index.questionState.currentQuestionId), answered = workspace.index.questionState.questions.filter(item => item.state === 'answered').length; + return { teamId: workspace.index.teamId, revision: workspace.index.revision, packageId: workspace.index.packageId, packageVersion: workspace.index.packageVersion, profile: UAR_PROFILE_V2, + counts: { agents: workspace.package.definitions.filter(item => item.document.kind === 'AgentDefinition').length, teams: workspace.package.definitions.filter(item => item.document.kind === 'TeamDefinition').length, workflows: workspace.package.definitions.filter(item => item.document.kind === 'WorkflowDefinition').length, diagnostics: diagnostics.length }, + complete: current === undefined && !workspace.migrationReceipt?.activationBlocked, + nextQuestion: current ? { id: current.id, document: current.document, pointer: current.pointer, question: current.prompt } : null, + questions: { answered, pending: workspace.index.questionState.questions.length - answered, currentQuestionId: workspace.index.questionState.currentQuestionId }, + diagnostics: { items, cursor: next < diagnostics.length ? String(next) : null, remaining: Math.max(0, diagnostics.length - next) } }; +} +function compareSemver(left, right) { + const parse = (value) => { const [core, pre] = value.split('-', 2); return [core.split('.').map(Number), pre === undefined ? [] : pre.split('.')]; }; + const [leftCore, leftPre] = parse(left), [rightCore, rightPre] = parse(right); + for (let index = 0; index < 3; index++) + if (leftCore[index] !== rightCore[index]) + return leftCore[index] - rightCore[index]; + if (!leftPre.length || !rightPre.length) + return leftPre.length ? -1 : rightPre.length ? 1 : 0; + for (let index = 0; index < Math.max(leftPre.length, rightPre.length); index++) { + const a = leftPre[index], b = rightPre[index]; + if (a === undefined || b === undefined) + return a === undefined ? -1 : 1; + if (a === b) + continue; + const an = /^[0-9]+$/.test(a), bn = /^[0-9]+$/.test(b); + if (an && bn) + return Number(a) - Number(b); + if (an !== bn) + return an ? -1 : 1; + return a.localeCompare(b); + } + return 0; +} +function replaceReferences(document, tuples) { + let changed = false; + const visit = (value) => { + if (!value || typeof value !== 'object') + return; + if (Array.isArray(value)) { + value.forEach(visit); + return; + } + if (typeof value.id === 'string' && typeof value.version === 'string') { + const tuple = tuples.find(item => item.id === value.id && item.fromVersion === value.version); + if (tuple) { + value.version = tuple.toVersion; + delete value.digest; + changed = true; + } + } + Object.values(value).forEach(visit); + }; + visit(document); + return changed; +} +export function reviseWorkspace(input) { + const project = projectRoot(input), sourcePath = workspacePath(input), destinationId = teamId(input, 'out'), destinationPath = workspacePath(input, 'out'), release = lock(project, sourcePath); + try { + const current = loadWorkspace(input); + assertRevision(current, expectedRevision(input)); + const nextVersion = text(input.nextVersion, 'nextVersion'); + if (!SEMVER.test(nextVersion) || compareSemver(nextVersion, current.index.packageVersion) <= 0) + throw new Error(`nextVersion must be greater than current package version ${current.index.packageVersion}`); + if (fs.existsSync(projectFile(project, `${destinationPath}/${INDEX_FILE}`))) + throw new Error(`Workspace already exists: ${destinationId}`); + const definitions = current.package.definitions.map(item => ({ path: item.path, document: structuredClone(item.document), raw: readFile(projectFile(project, `${sourcePath}/${item.path}`)) })); + const edits = input.edits === undefined ? [] : input.edits; + if (!Array.isArray(edits)) + throw new Error('edits must be an array'); + const changedPaths = new Set(), tuples = []; + for (const [position, raw] of edits.entries()) { + const edit = object(raw, `edits[${position}]`), file = relativeFile(text(edit.path, `edits[${position}].path`)); + if (changedPaths.has(file)) + throw new Error(`Duplicate revision edit: ${file}`); + const selected = definitions.find(item => item.path === file); + if (!selected) + throw new Error(`Revision edit path is not a definition: ${file}`); + const document = object(edit.document, `edits[${position}].document`); + if (document.kind !== selected.document.kind || document.id !== selected.document.id) + throw new Error(`Revision edit cannot change definition kind or identity: ${file}`); + const oldVersion = text(selected.document.version, `${file}.version`), newVersion = text(document.version, `${file}.version`); + if (!SEMVER.test(newVersion) || compareSemver(newVersion, oldVersion) <= 0) + throw new Error(`Edited definition ${file} requires a strictly greater semantic version`); + delete document.contentDigest; + selected.document = structuredClone(document); + changedPaths.add(file); + tuples.push({ id: text(document.id, `${file}.id`), fromVersion: oldVersion, toVersion: newVersion }); + } + for (let pass = 0; pass <= definitions.length; pass++) { + let propagated = false; + for (const selected of definitions) { + if (!replaceReferences(selected.document, tuples) || changedPaths.has(selected.path)) + continue; + const oldVersion = text(current.package.definitions.find(item => item.path === selected.path).document.version, `${selected.path}.version`); + if (compareSemver(nextVersion, oldVersion) <= 0) + throw new Error(`Dependency update requires ${nextVersion} to exceed ${selected.path} version ${oldVersion}`); + selected.document.version = nextVersion; + delete selected.document.contentDigest; + changedPaths.add(selected.path); + tuples.push({ id: text(selected.document.id, `${selected.path}.id`), fromVersion: oldVersion, toVersion: nextVersion }); + propagated = true; + } + if (!propagated) + break; + } + const manifest = structuredClone(current.package.manifest); + manifest.version = nextVersion; + delete manifest.contentDigest; + delete manifest.files; + delete manifest.lock; + replaceReferences(manifest, tuples); + compileUarPackage({ manifest, definitions: definitions.map(item => ({ path: item.path, document: item.document })) }, current.migrationReceipt); + const nextRevision = current.index.revision + 1; + const index = validateIndex({ ...current.index, revision: nextRevision, teamId: destinationId, packageVersion: nextVersion, base: { teamId: current.index.teamId, revision: current.index.revision, packageVersion: current.index.packageVersion }, questionState: questionState(current.index.manifest, manifest, definitions, current.index.bindingIntent, current.index.questionState) }); + const changes = new Map(); + stage(changes, project, `${destinationPath}/${INDEX_FILE}`, json(index)); + stage(changes, project, `${destinationPath}/${index.manifest}`, json(manifest)); + for (const definition of definitions) + stage(changes, project, `${destinationPath}/${definition.path}`, changedPaths.has(definition.path) ? json(definition.document) : definition.raw); + if (index.migrationReceipt) + stage(changes, project, `${destinationPath}/${index.migrationReceipt}`, readFile(projectFile(project, `${sourcePath}/${index.migrationReceipt}`))); + const recovery = commitChanges(project, [...changes.values()], { operation: 'workspace-revise', teamId: destinationId, baseTeamId: current.index.teamId, beforeRevision: current.index.revision, afterRevision: nextRevision }); + return { workspace: destinationId, path: destinationPath, ...(index.base ? { base: index.base } : {}), revision: nextRevision, packageVersion: nextVersion, changedDefinitions: [...changedPaths].sort(), unchangedDefinitions: definitions.filter(item => !changedPaths.has(item.path)).map(item => item.path).sort(), recovery }; + } + finally { + release(); + } +} diff --git a/skills/delivery-cadence/SKILL.md b/skills/delivery-cadence/SKILL.md index 6750ee4..5761fde 100644 --- a/skills/delivery-cadence/SKILL.md +++ b/skills/delivery-cadence/SKILL.md @@ -4,7 +4,7 @@ description: Operate timed usable deliveries with child-phase recovery, build-an license: MIT compatibility: Node.js 22 or newer. Build tools belong to the selected project. Optional KBD, Compass, memory and native goal capabilities are detected, never assumed. metadata: - version: "1.1.1" + version: "1.1.2" tags: "delivery, cadence, kbd, recovery" --- diff --git a/skills/delivery-cadence/references/profile.md b/skills/delivery-cadence/references/profile.md index 7becc9c..e41986a 100644 --- a/skills/delivery-cadence/references/profile.md +++ b/skills/delivery-cadence/references/profile.md @@ -40,6 +40,7 @@ The feature checkpoint must be configured. `start.checkpoints` can supply the co - `observe`: import an actual interval with `kind`, `startedAt`, `finishedAt`. `{ "reopened":{"tasks":["id"]}, "source":"ledger-reference" }` records reopened canonical work. Do not reconstruct missing duration from memory. - `ready`: `{ "codeComplete":true }` freezes the current sources after the entire increment is wired. Unresolved child work blocks it. - `checkpoint`: `{ "id":"build" }`, then launch, then the selected feature operation. Repeated builds require `reason`. Preserve earlier attempts. +- When the completed operation needs a different process timeout, `checkpoint` may include positive integer `timeoutMs` and a mandatory `reason`. The attempt records both configured and requested limits; it preserves the frozen command and scope, continuous iteration clock, and hard run budget. It does not authorize extra functionality or bypass an approval. - `finish`: supply `outcome`, `completion` with separate canonical tasks/changes/phases, and optional local-file `artifacts`. KBD/goal completions require `canonicalEvidence` pointing to the supported ledger export. All required successful receipts must match frozen sources. Failed delivery is repaired before new scope. - `publication`: supply `iterationId`, `outcome`, local `artifacts`, and a receipt file identifying version, frozen sources, checksummed published artifacts, and required website/metadata/acceptance evidence. Workflow dispatch is not publication. - `review`: `{ "acknowledged":true }` only after real operator acknowledgement. diff --git a/skills/delivery-cadence/scripts/lib/checkpoints.mjs b/skills/delivery-cadence/scripts/lib/checkpoints.mjs index 4b56369..82b7cac 100644 --- a/skills/delivery-cadence/scripts/lib/checkpoints.mjs +++ b/skills/delivery-cadence/scripts/lib/checkpoints.mjs @@ -95,6 +95,10 @@ export async function runCheckpoint(root, state, iteration, input, commandId) { if (!step) throw new Error(`No configured checkpoint ${input.id}`); const previous = iteration.checkpoints.find((item) => item.commandId === commandId); if (previous) return previous; + if (input.timeoutMs !== undefined && (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0 || typeof input.reason !== 'string' || !input.reason.trim())) { + throw new Error('A checkpoint timeout adjustment requires positive integer milliseconds and a recorded reason'); + } + const requestedTimeoutMs = input.timeoutMs ?? step.timeoutMs ?? 14_400_000; const repeated = iteration.checkpoints.some((item) => item.id === step.id); if (step.kind === 'build' && repeated && (iteration.contractVersion ?? 1) >= 2 && (typeof input.reason !== 'string' || !input.reason.trim())) { throw new Error('Repeated builds require a reason identifying the repair or changed release input'); @@ -127,6 +131,7 @@ export async function runCheckpoint(root, state, iteration, input, commandId) { const attempt = { id: step.id, attemptId: randomUUID(), commandId, kind: step.kind, purpose, status: 'running', startedAt, ...(purpose === 'feature' ? { featureOperationId: iteration.featureOperation.id } : {}), reason: input.reason ?? (repeated ? 'Unspecified legacy retry' : 'Initial delivery checkpoint'), + configuredTimeoutMs: step.timeoutMs ?? 14_400_000, requestedTimeoutMs, sourceRefs: iteration.sourceRefs, command: step.command, args: step.args, cwd: path.resolve(step.cwd), hostname: os.hostname() }; iteration.checkpoints.push(attempt); iteration.status = 'checkpoint-running'; await saveEvent(root, state, 'checkpoint.started', { iterationId: iteration.id, attemptId: attempt.attemptId }); @@ -144,7 +149,7 @@ export async function runCheckpoint(root, state, iteration, input, commandId) { ? Date.parse(state.startedAt) + state.profile.budgets.maxRunMinutes * 60_000 - Date.now() : Infinity; try { if (remaining <= 0) throw new Error('Run budget exhausted before checkpoint dispatch'); - const result = await runCommand({ ...step, timeoutMs: Math.min(step.timeoutMs ?? 14_400_000, remaining) }, { + const result = await runCommand({ ...step, timeoutMs: Math.min(requestedTimeoutMs, remaining) }, { cwd: step.cwd, signal: controller.signal, onSpawn(pid) { attempt.pid = pid; spawnSave = saveEvent(root, state, 'checkpoint.process', { attemptId: attempt.attemptId, pid }); }, onOutput(stream, text) { diff --git a/tools/liter-llm b/tools/liter-llm index 4f25d39..0617979 160000 --- a/tools/liter-llm +++ b/tools/liter-llm @@ -1 +1 @@ -Subproject commit 4f25d39f0075656b70bf945880d6033beda6d663 +Subproject commit 0617979022aea621dd13541dee07ad84ffcf7d21