Repository navigation
feat: add Terminal49 agent trust stack #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7849f64
a580d85
702c8e9
74af142
6d19283
8aab700
e786535
28f37a4
13d9919
65a1105
1e8c1cb
3e558a9
8b6a162
984a1c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| description: Terminal49/API paved path and verification gate | ||
| alwaysApply: true | ||
| --- | ||
|
|
||
| # Agent trust gate | ||
|
|
||
| - Extend OpenAPI endpoints, npm packages, and MCP tools through | ||
| `skills/agent-trust/references/paved-path.md`. | ||
| - Keep `skills/agent-trust/feature-map.json` synchronized with MCP tool | ||
| registrations, SDK package exports, and non-generated SDK/MCP docs routes. | ||
| - Run `npm run agent-verify` before handing off code changes. Do not claim a | ||
| passing result from `agent-verify:quick`. | ||
| - Keep generated files generated and credentials out of this public repository. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| description: Repository-local P-Stack routing | ||
| alwaysApply: true | ||
| --- | ||
|
|
||
| # P-Stack | ||
|
|
||
| For non-trivial Terminal49/API work, use the installed P-Stack plugin's | ||
| `/poteto-mode` workflow when available. Use `/how` for unfamiliar subsystems | ||
| and `/reflect` to turn durable lessons into executable guards. The agent-trust | ||
| rule owns the repository verification gate. | ||
|
|
||
| Model and reasoning-budget choices belong in the user-local file created by | ||
| `/setup-pstack`; never commit personal model slugs. |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { spawnSync } from 'node:child_process'; | ||
| import { resolve } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const root = resolve(fileURLToPath(new URL('..', import.meta.url))); | ||
| const mode = process.argv[2]; | ||
| if (mode && mode !== '--quick') { | ||
| console.error('Usage: bin/agent-verify.mjs [--quick]'); | ||
| process.exit(2); | ||
| } | ||
|
|
||
| const steps = [ | ||
| { | ||
| name: 'Node runtime', | ||
| run: () => { | ||
| const [major, minor] = process.versions.node.split('.').map(Number); | ||
| if (major !== 24 || minor < 11) { | ||
| throw new Error( | ||
| `Node 24.11+ is required; received ${process.versions.node}`, | ||
| ); | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: 'Agent trust guards', | ||
| command: ['node', 'bin/check-agent-trust.mjs'], | ||
| }, | ||
| { | ||
| name: 'Vite+ and TypeScript checks', | ||
| command: ['npx', '--no-install', 'vp', 'run', 'check'], | ||
| }, | ||
| { | ||
| name: 'Workspace builds', | ||
| command: ['npx', '--no-install', 'vp', 'run', 'build'], | ||
| }, | ||
| ...(mode === '--quick' | ||
| ? [] | ||
| : [ | ||
| { | ||
| name: 'Workspace tests', | ||
| command: ['npx', '--no-install', 'vp', 'run', 'test'], | ||
| }, | ||
| ]), | ||
| { | ||
| name: 'OpenAPI SDK type generation', | ||
| command: ['node', 'bin/check-openapi-types.mjs'], | ||
| }, | ||
| { | ||
| name: 'MCP initialize and tools/list smoke', | ||
| command: ['npm', 'run', 'test:protocol', '--workspace', '@terminal49/mcp'], | ||
| env: { MCP_PROTOCOL_VERSION: '2025-06-18' }, | ||
| }, | ||
| ]; | ||
|
|
||
| function runStep(step) { | ||
| const startedAt = performance.now(); | ||
| process.stdout.write(`\n[RUN ] ${step.name}\n`); | ||
| if (step.run) { | ||
| step.run(); | ||
| } else { | ||
| const [command, ...args] = step.command; | ||
| const result = spawnSync(command, args, { | ||
| cwd: root, | ||
| env: { ...process.env, ...step.env }, | ||
| stdio: 'inherit', | ||
| }); | ||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
| if (result.status !== 0) { | ||
| throw new Error(`command exited ${result.status ?? 'without a status'}`); | ||
| } | ||
| } | ||
| const duration = Math.round(performance.now() - startedAt); | ||
| process.stdout.write(`[PASS] ${step.name} (${duration} ms)\n`); | ||
| } | ||
|
|
||
| for (const step of steps) { | ||
| try { | ||
| runStep(step); | ||
| } catch (error) { | ||
| console.error( | ||
| `[FAIL] ${step.name}: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| console.log( | ||
| `\nPASS agent-verify${mode === '--quick' ? ' --quick' : ''}: ${steps.length} steps`, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { existsSync, readFileSync, readdirSync } from 'node:fs'; | ||
| import { join, relative, resolve } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const root = resolve(fileURLToPath(new URL('..', import.meta.url))); | ||
|
|
||
| function readJson(path) { | ||
| return JSON.parse(readFileSync(resolve(root, path), 'utf8')); | ||
| } | ||
|
|
||
| function sorted(values) { | ||
| return [...values].sort((left, right) => left.localeCompare(right)); | ||
| } | ||
|
|
||
| function normalizedVersion(version) { | ||
| return version.replace(/^=/, ''); | ||
| } | ||
|
|
||
| function assertEqual(label, actual, expected) { | ||
| const actualJson = JSON.stringify(sorted(actual)); | ||
| const expectedJson = JSON.stringify(sorted(expected)); | ||
| if (actualJson !== expectedJson) { | ||
| throw new Error( | ||
| `${label} drifted.\nExpected: ${expectedJson}\nActual: ${actualJson}`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function collectFiles(directory) { | ||
| const files = []; | ||
| for (const entry of readdirSync(directory, { withFileTypes: true })) { | ||
| const path = join(directory, entry.name); | ||
| if (entry.isDirectory()) { | ||
| files.push(...collectFiles(path)); | ||
| } else { | ||
| files.push(path); | ||
| } | ||
| } | ||
|
Comment on lines
+33
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
To find Vite config files, this function walks every file under Prompt To Fix With AIThis is a comment left during a code review.
Path: bin/check-agent-trust.mjs
Line: 33-40
Comment:
**Workspace scan includes generated files**
To find Vite config files, this function walks every file under `packages` and `sdks` without excluding generated output or nested dependencies. That adds unnecessary filesystem work to both verification modes and could mistake a dependency’s config for a workspace-owned one. Limit the scan to workspace source and config locations.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| return files; | ||
| } | ||
|
|
||
| function collectStrings(value) { | ||
| if (typeof value === 'string') { | ||
| return [value]; | ||
| } | ||
| if (Array.isArray(value)) { | ||
| return value.flatMap(collectStrings); | ||
| } | ||
| if (value && typeof value === 'object') { | ||
| return Object.values(value).flatMap(collectStrings); | ||
| } | ||
| return []; | ||
| } | ||
|
|
||
| function checkFeatureMap() { | ||
| const featureMap = readJson('skills/agent-trust/feature-map.json'); | ||
| const serverSource = readFileSync( | ||
| resolve(root, 'packages/mcp/src/server.ts'), | ||
| 'utf8', | ||
| ); | ||
| const registeredTools = [ | ||
| ...serverSource.matchAll(/server\.registerTool\(\s*'([^']+)'/g), | ||
| ].map((match) => match[1]); | ||
|
Comment on lines
+59
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The guard reads only Prompt To Fix With AIThis is a comment left during a code review.
Path: bin/check-agent-trust.mjs
Line: 59-65
Comment:
**Tool registrations can escape guard**
The guard reads only `server.ts` and matches single-quoted `server.registerTool(...)` calls. An otherwise valid registration using double quotes or another module would not be counted, so verification could pass while the feature map omits a public tool. Make the check account for equivalent registration forms and locations.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Comment on lines
+63
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a new tool is registered through a constant such as AGENTS.md reference: AGENTS.md:L78-L81 Useful? React with 👍 / 👎. |
||
| assertEqual( | ||
| 'MCP feature map', | ||
| featureMap.mcpTools.map((tool) => tool.name), | ||
| registeredTools, | ||
| ); | ||
|
|
||
| for (const tool of featureMap.mcpTools) { | ||
| if (!existsSync(resolve(root, tool.implementation))) { | ||
| throw new Error(`Missing MCP implementation: ${tool.implementation}`); | ||
| } | ||
| } | ||
|
|
||
| const sdkPackage = readJson('sdks/typescript-sdk/package.json'); | ||
| assertEqual( | ||
| 'SDK entrypoint map', | ||
| featureMap.sdkEntrypoints.map((entry) => entry.export), | ||
| Object.keys(sdkPackage.exports), | ||
| ); | ||
| for (const entry of featureMap.sdkEntrypoints) { | ||
| if (!existsSync(resolve(root, entry.source))) { | ||
| throw new Error(`Missing SDK entrypoint source: ${entry.source}`); | ||
| } | ||
| } | ||
|
|
||
| const docsConfig = readJson('docs/docs.json'); | ||
| const docsRoutes = collectStrings(docsConfig.navigation).filter( | ||
| (route) => | ||
| route === 'api-docs/in-depth-guides/mcp' || | ||
| route.startsWith('mcp/') || | ||
| (route.startsWith('sdk/') && !route.startsWith('sdk/reference/')), | ||
| ); | ||
| assertEqual('Docs route map', featureMap.docsRoutes, docsRoutes); | ||
| for (const route of featureMap.docsRoutes) { | ||
| if (!existsSync(resolve(root, `docs/${route}.mdx`))) { | ||
| throw new Error(`Missing docs route source: docs/${route}.mdx`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function checkToolchain() { | ||
| const rootPackage = readJson('package.json'); | ||
| const vitePlusPackage = readJson('node_modules/vite-plus/package.json'); | ||
| const expected = vitePlusPackage.dependencies; | ||
| const workspaces = [ | ||
| 'packages/mcp/package.json', | ||
| 'sdks/typescript-sdk/package.json', | ||
| 'sdks/typescript-sdk-cli/package.json', | ||
| ]; | ||
|
Comment on lines
+109
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When another package is added under the root AGENTS.md reference: AGENTS.md:L69-L72 Useful? React with 👍 / 👎. |
||
|
|
||
| if (rootPackage.devDependencies['vite-plus'] !== vitePlusPackage.version) { | ||
| throw new Error( | ||
| 'Root vite-plus version does not match the installed tool.', | ||
| ); | ||
| } | ||
| if ( | ||
| rootPackage.devDependencies['@oxlint/plugins'] !== | ||
| normalizedVersion(expected['@oxlint/plugins']) | ||
| ) { | ||
| throw new Error('@oxlint/plugins is not coupled to vite-plus.'); | ||
| } | ||
| if (rootPackage.overrides.vitest !== expected.vitest) { | ||
| throw new Error('Vitest override is not coupled to vite-plus.'); | ||
| } | ||
| if ( | ||
| rootPackage.overrides.vite !== | ||
| `npm:@voidzero-dev/vite-plus-core@${vitePlusPackage.version}` | ||
| ) { | ||
| throw new Error('Vite core override is not coupled to vite-plus.'); | ||
| } | ||
|
|
||
| for (const workspacePath of workspaces) { | ||
| const workspace = readJson(workspacePath); | ||
| if (workspace.devDependencies['vite-plus'] !== vitePlusPackage.version) { | ||
| throw new Error(`${workspacePath} has a mismatched vite-plus version.`); | ||
| } | ||
| if (workspace.devDependencies['@vitest/coverage-v8'] !== expected.vitest) { | ||
| throw new Error( | ||
| `${workspacePath} has a mismatched Vitest coverage version.`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function checkConfigOwnership() { | ||
| const nestedConfigs = ['packages', 'sdks'] | ||
| .flatMap((directory) => collectFiles(resolve(root, directory))) | ||
| .filter((path) => /(?:^|\/)vite\.config\.[cm]?[jt]s$/.test(path)) | ||
| .map((path) => relative(root, path)); | ||
| if (nestedConfigs.length > 0) { | ||
| throw new Error( | ||
| `Workspace Vite configs are not allowed: ${nestedConfigs.join(', ')}`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| checkFeatureMap(); | ||
| checkToolchain(); | ||
| checkConfigOwnership(); | ||
| console.log('PASS agent trust guards'); | ||
| } catch (error) { | ||
| console.error( | ||
| `FAIL agent trust guards: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| process.exitCode = 1; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join, resolve } from 'node:path'; | ||
| import { spawnSync } from 'node:child_process'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const root = resolve(fileURLToPath(new URL('..', import.meta.url))); | ||
| const temporaryDirectory = mkdtempSync(join(tmpdir(), 't49-openapi-')); | ||
| const generatedPath = join(temporaryDirectory, 'terminal49.ts'); | ||
|
|
||
| try { | ||
| const result = spawnSync( | ||
| 'npx', | ||
| [ | ||
| '--no-install', | ||
| 'openapi-typescript', | ||
| 'docs/openapi.json', | ||
| '-o', | ||
| generatedPath, | ||
| ], | ||
| { | ||
| cwd: root, | ||
| encoding: 'utf8', | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| }, | ||
| ); | ||
| if (result.status !== 0) { | ||
| throw new Error(result.stderr.trim() || 'openapi-typescript failed'); | ||
| } | ||
|
|
||
| const expected = readFileSync( | ||
| resolve(root, 'sdks/typescript-sdk/src/generated/terminal49.ts'), | ||
| 'utf8', | ||
| ); | ||
| const actual = readFileSync(generatedPath, 'utf8'); | ||
| if (actual !== expected) { | ||
| throw new Error( | ||
| 'OpenAPI-generated SDK types are stale. Run `npm run generate:types --workspace @terminal49/sdk`.', | ||
| ); | ||
| } | ||
|
|
||
| console.log('PASS OpenAPI SDK type generation'); | ||
| } catch (error) { | ||
| console.error( | ||
| `FAIL OpenAPI SDK type generation: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| process.exitCode = 1; | ||
| } finally { | ||
| rmSync(temporaryDirectory, { recursive: true, force: true }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The repository declares support for Node
24.x, but this required completion gate rejects Node 24.0–24.10. Contributors using those supported versions cannot passnpm run agent-verify. Align the verifier’s minimum with the declared engine range.Prompt To Fix With AI