diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 000000000..29c8ed260 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,22 @@ +name: Cordis verification +on: + pull_request: + branches: [cordis] + push: + branches: ['feature/**', 'fix/**'] +permissions: + contents: read +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.4.4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 24.19.0 + - run: bun install --frozen-lockfile + - run: bun run verify diff --git a/CHANGELOG.md b/CHANGELOG.md index c2adb1c30..143439a4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Add native API container provider and exact service definition (#617). + ### Fixed - Restore the existing API committer policy to Cordis (#620). diff --git a/README.md b/README.md new file mode 100644 index 000000000..62083d302 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Cordis Api + + +## Task 3B: Installed API root and container provider + +Legacy src/api/support/server.ts remains this repository's capability. ApiApplication is the default native Cordis root, mounting selected HTTP, operation/worker, Identity and Admin class plugins. Its container provider is also a native class plugin in the host-worker context, with actual server/assets readiness. GET /health/ready is a bounded readiness endpoint; authenticated GET /v1/platform/status and /v1/platform/applications/{id} invoke the public Deployment worker client. Admin/OIDC and /identity routing terminate at the owned HTTPS edge. Local worker authority never arrives in ordinary user input. The API gets only the private worker socket directory, never Docker.sock. The package retains AGPL-3.0 licensing. + +Task 3B is authorized after the maintainer reviewed the isolated setup. Tracking: #617. Detailed integration plan: https://github.com/treeseed-ai/platform/blob/cordis/docs/superpowers/plans/2026-10-24-task3b-installed-composition.md. Implementation evidence is recorded separately; these requirements are not a passing-test claim. + +Development: Bun 1.4.4, Node 24.19.0, TypeScript 5.9.3, native Cordis 4.0.0-rc.12. Run bun run verify (semantic class/source check, strict compiler, build and nonempty unit/integration/program smoke). Installed acceptance uses genuine exact package/OCI bytes, real identity/database/API/HTTPS and explicit isolated certificate trust. No global trust mutation or legacy service changes. All handwritten source, tools and tests are strict TypeScript classes. + +All capabilities that can be plugins, including container services, are native Cordis class plugins with typed swappable contracts. No parallel DI graph. Keep independent repository/public package boundaries, exact archive pins and original licenses. Migration PRs target cordis with Part of issue references. All issues remain open until authorized release; installer/Agent/generalized recovery are later tasks. + +Delivery is split into two reviewable changes under #617: the native container provider/service definition baseline, then the API server root and authenticated installed interfaces. Each targets cordis and carries its own checks. The server follow-up consumes the first slice and the exact Identity, Admin and Deployment archives. This baseline does not claim an implemented server. diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..48c8a55a9 --- /dev/null +++ b/bun.lock @@ -0,0 +1,46 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "@treeseed-cordis/api", + "dependencies": { + "cordis": "4.0.0-rc.12", + }, + "devDependencies": { + "@treeseed-cordis/sdk": "file:vendor/sdk.tgz", + "@types/bun": "1.4.4", + "@types/node": "24.10.1", + "typescript": "5.9.3", + }, + "peerDependencies": { + "@treeseed-cordis/sdk": "0.0.0-cordis.4", + }, + }, + }, + "packages": { + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@treeseed-cordis/sdk": ["@treeseed-cordis/sdk@vendor/sdk.tgz", { "dependencies": { "cordis": "4.0.0-rc.12", "jsonc-parser": "3.3.1", "semver": "7.8.5", "zod": "4.6.7" } }, "sha512-2EoB8+NYfkLyjvEqRW/HhV8xCbokGCUJkqWfBid7kQp5/R2CowBj6EWnVv1wtKFZVoPI3qiLYMWWYAp1qxzPcA=="], + + "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="], + + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + + "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="], + + "cordis": ["cordis@4.0.0-rc.12", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "cosmokit": "^1.8.1" }, "peerDependencies": { "@cordisjs/plugin-include": "^1.2.0", "@cordisjs/plugin-loader": "^1.0.0-rc.9" }, "optionalPeers": ["@cordisjs/plugin-include", "@cordisjs/plugin-loader"], "bin": "bin.js" }, "sha512-/ZtRivYxr5sffTULPewHuX5kEy5sWi8oADHSpnt0KjAd8uKRIj+kv4fh3lxtutIfVlgwJVFVXWOQp/gYa+kIOQ=="], + + "cosmokit": ["cosmokit@1.8.1", "", {}, "sha512-PDBv4l90xZKrUsZ0vtoycgZpO/j4iFsqJXrAxsyBDsnQRI7ZMJXIjgDJsKNjd5L8jnVnnlrDCdhkFbTncgCVjQ=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..9bed09268 --- /dev/null +++ b/package.json @@ -0,0 +1,139 @@ +{ + "name": "@treeseed-cordis/api", + "version": "0.0.0-cordis.1", + "private": true, + "type": "module", + "license": "AGPL-3.0-only", + "packageManager": "bun@1.4.4", + "engines": { + "node": ">=24.19.0" + }, + "exports": { + ".": { + "types": "./dist/src/container.d.ts", + "default": "./dist/src/container.js" + }, + "./container": { + "types": "./dist/src/container.d.ts", + "default": "./dist/src/container.js" + } + }, + "files": [ + "dist/src", + "README.md", + "LICENSE" + ], + "scripts": { + "check": "bun node_modules/@treeseed-cordis/sdk/dist/src/source-policy.js check", + "build": "bun node_modules/@treeseed-cordis/sdk/dist/src/source-policy.js build && bun dist/src/container.js --metadata", + "test:unit": "bun test ./tests --test-name-pattern '^unit:'", + "test:integration": "bun test ./tests --test-name-pattern '^integration:'", + "test:smoke": "bun test ./tests --test-name-pattern '^smoke:'", + "verify": "bun run check && bun run build && bun run test:unit && bun run test:integration && bun run test:smoke" + }, + "peerDependencies": { + "@treeseed-cordis/sdk": "0.0.0-cordis.4" + }, + "devDependencies": { + "@treeseed-cordis/sdk": "file:vendor/sdk.tgz", + "typescript": "5.9.3", + "@types/bun": "1.4.4", + "@types/node": "24.10.1" + }, + "dependencies": { + "cordis": "4.0.0-rc.12" + }, + "cordisCandidate": { + "sdk": { + "repository": "https://github.com/treeseed-ai/sdk.git", + "commit": "c03a2b0eab89786439eddea3cff8161e85676b3e", + "sha256": "d6d0f2f4379d2d95403a226d20899ad22a78a11ccdbb4106116ef0a925295d1b" + } + }, + "cordis": { + "id": "node-api", + "pluginId": "node-api", + "capability": "container.api", + "contractVersion": "0.3.0-cordis.1", + "target": "deployment", + "provide": [ + "container.api" + ], + "inject": [ + "deployment.engine", + "container.database", + "container.identity" + ] + }, + "cordisService": { + "role": "api", + "service": { + "command": [ + "--serve", + "--configuration", + "/run/secrets/api-config" + ], + "user": "installation-owner", + "workerSocket": true, + "volumes": [], + "secrets": [ + { + "source": "api-config", + "target": "/run/secrets/api-config" + } + ], + "portTarget": null + }, + "slots": { + "api-config": { + "kind": "json", + "value": { + "installationId": { + "$installationId": true + }, + "origin": { + "$origin": "" + }, + "issuer": { + "$origin": "/identity/realms/cordis" + }, + "identityInternalOrigin": "http://identity:8080", + "clientId": "cordis-admin", + "clientSecret": { + "$secret": "oidc-client" + }, + "databaseUrl": { + "$template": [ + "postgresql://cordis:", + { + "$secret": "database-password" + }, + "@database:5432/cordis" + ] + }, + "bootstrapSubject": { + "$secret": "bootstrap-subject" + }, + "workerSocket": "/run/cordis-host/worker.sock", + "assetDigest": { + "$assetDigest": true + } + } + } + }, + "readiness": { + "kind": "http-json", + "path": "/health/ready", + "expected": { + "ready": true, + "installationId": { + "$installationId": true + }, + "assetDigest": { + "$assetDigest": true + }, + "nativeComposition": true + } + } + } +} diff --git a/src/container.ts b/src/container.ts new file mode 100644 index 000000000..668f89e92 --- /dev/null +++ b/src/container.ts @@ -0,0 +1,20 @@ +import {Service} from 'cordis'; +import {readFileSync,writeFileSync} from 'node:fs'; +import {InstalledSchemas,InstalledDeclarations,InstalledTokens,PlatformSchemas,type ContainerEngine,type ReadyContainer,type ProviderDeclaration} from '@treeseed-cordis/sdk'; +import {Context} from '@treeseed-cordis/sdk/runtime'; +export class NodeApiContainerPlugin implements ReadyContainer { + static readonly declaration=InstalledDeclarations.service('node-api','container.api',['deployment.engine','container.database','container.identity']); + static readonly definition=InstalledSchemas.definition.parse({role:'api',service:{command:['--serve','--configuration','/run/secrets/api-config'],user:'installation-owner',workerSocket:true,volumes:[],secrets:[{source:'api-config',target:'/run/secrets/api-config'}],portTarget:null},slots:{'api-config':{kind:'json',value:{installationId:{$installationId:true},origin:{$origin:''},issuer:{$origin:'/identity/realms/cordis'},identityInternalOrigin:'http://identity:8080',clientId:'cordis-admin',clientSecret:{$secret:'oidc-client'},databaseUrl:{$template:['postgresql://cordis:',{$secret:'database-password'},'@database:5432/cordis']},bootstrapSubject:{$secret:'bootstrap-subject'},workerSocket:'/run/cordis-host/worker.sock',assetDigest:{$assetDigest:true}}}},readiness:{kind:'http-json',path:'/health/ready',expected:{ready:true,installationId:{$installationId:true},assetDigest:{$assetDigest:true},nativeComposition:true}}}); + static readonly provide=this.declaration.provide;static readonly inject=this.declaration.inject;static readonly Config=PlatformSchemas.empty; + private readonly engine:ContainerEngine; + constructor(private readonly context:Context){this.engine=InstalledTokens.engine.parse(Reflect.get(context,InstalledTokens.engine.id));} + async [Service.init]():Promise{await this.engine.start('api',NodeApiContainerPlugin.definition);if(!await this.ready())throw new Error('API readiness failed');this.context.provide(InstalledTokens.api.id,this);} + identity():Readonly<{installationId:string;composition:string}>{return this.engine.identity();} + ready():Promise{return this.engine.ready('api');} +} +export {NodeApiContainerPlugin as Provider}; +export class ApiContainerBuild { + static metadata():ProviderDeclaration{return NodeApiContainerPlugin.declaration;} + static run():void{if(!import.meta.main)return;if(process.argv[2]==='--metadata'){const metadata=JSON.parse(readFileSync('package.json','utf8')) as Record;metadata.cordis=this.metadata();metadata.cordisService=NodeApiContainerPlugin.definition;writeFileSync('package.json',JSON.stringify(metadata,null,2)+'\n');}else if(process.argv[2]==='--describe')process.stdout.write(JSON.stringify(this.metadata())+'\n');else{process.stderr.write('Unsupported API container program invocation.\n');process.exitCode=2;}} +} +ApiContainerBuild.run(); diff --git a/tests/container.test.ts b/tests/container.test.ts new file mode 100644 index 000000000..267dfa424 --- /dev/null +++ b/tests/container.test.ts @@ -0,0 +1,11 @@ +import {test,expect} from 'bun:test'; +import {spawnSync} from 'node:child_process'; +import {readFileSync} from 'node:fs'; +import {ApiContainerBuild,NodeApiContainerPlugin} from '../src/container.js'; +import {InstalledSchemas} from '@treeseed-cordis/sdk'; +class ApiContainerTests { + program():void{const result=spawnSync('node',['dist/src/container.js','--describe'],{encoding:'utf8'});expect(result.status).toBe(0);expect(result.stderr).toBe('');expect(JSON.parse(result.stdout).capability).toBe('container.api');} + definition():void{const definition=InstalledSchemas.definition.parse(NodeApiContainerPlugin.definition);expect(definition.service.workerSocket).toBe(true);expect(definition.service.command).toEqual(['--serve','--configuration','/run/secrets/api-config']);expect(definition.service.environment).toBeUndefined();expect(NodeApiContainerPlugin.inject).toEqual(['deployment.engine','container.database','container.identity']);} + metadata():void{const metadata=JSON.parse(readFileSync('package.json','utf8'));expect(metadata.cordis).toEqual(ApiContainerBuild.metadata());expect(metadata.cordisService).toEqual(NodeApiContainerPlugin.definition);} +} +const suite=new ApiContainerTests();test('smoke: built API container program declares native provider',suite.program.bind(suite));test('unit: API container declares bounded configuration and dependencies',suite.definition.bind(suite));test('integration: package projects actual container class definition',suite.metadata.bind(suite)); diff --git a/vendor/sdk.tgz b/vendor/sdk.tgz new file mode 100644 index 000000000..ab562d4f9 Binary files /dev/null and b/vendor/sdk.tgz differ