Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
- name: Build the verified native TreeDX engine
working-directory: .treeseed/tools/treedx/apps/api
run: MIX_ENV=dev mix compile
- uses: treeseed-ai/sdk/.github/actions/install-exact-sdk@69c07896708deaaed228cc6e14f12c41e5289981
- uses: treeseed-ai/sdk/.github/actions/install-exact-sdk@ee3d869cb38a5e74485da58553db4057e5cdcdd6
env:
NODE_ENV: production
with:
Expand Down
27 changes: 7 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"@react-email/render": "^2.0.8",
"@treeseed/deployment": "https://github.com/treeseed-ai/deployment/releases/download/0.1.0-rc.321/treeseed-deployment-runtime-0.1.0-rc.321.tgz",
"@treeseed/identity": "0.1.0-rc.19",
"@treeseed/sdk": "git+https://github.com/treeseed-ai/sdk.git#69c07896708deaaed228cc6e14f12c41e5289981",
"@treeseed/sdk": "git+https://github.com/treeseed-ai/sdk.git#ee3d869cb38a5e74485da58553db4057e5cdcdd6",
"@treeseed/treedx": "0.3.0-rc.18",
"drizzle-orm": "^0.45.2",
"hono": "4.13.3",
Expand Down
13 changes: 9 additions & 4 deletions src/api/auth/browser/api-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { protectedResourceMetadataSchema } from '@treeseed/sdk/identity';
import type { createApiIdentityRuntime } from './runtime.ts';
import { installIdentityBrowserRoutes } from './routes.ts';
import { installControlPlaneResourceRoutes } from '../../control-plane/http/resource-routes.ts';
import { controlPlaneOperations } from '../../control-plane/catalog/index.ts';

/** One resource server plus the workload-authenticated BFF bridge. It cannot
* issue passwords, API tokens or OAuth grants. The approved issuer does that.
Expand All @@ -13,10 +14,14 @@ export function installApiIdentityRoutes<E extends Env>(app: Hono<E>, runtime: A
mcpBusForPrincipal?: Parameters<typeof installControlPlaneResourceRoutes>[4];
}) {
const metadata = protectedResourceMetadataSchema.parse(runtime.metadata);
for (const path of ['/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource/mcp']) {
app.get(path, c => c.json(metadata, 200, { 'cache-control': 'public, max-age=60' }));
}
const registry = options.registry ?? controlPlaneOperations;
const mcpScopes = new Set<string>(registry.catalog.operations
.filter(operation => operation.surfaces.includes('mcp_tool') || operation.surfaces.includes('mcp_resource'))
.flatMap(operation => operation.oauthScopes));
const mcpMetadata = { ...metadata, scopes_supported: (metadata.scopes_supported ?? []).filter(scope => mcpScopes.has(scope)) };
app.get('/.well-known/oauth-protected-resource', c => c.json(metadata, 200, { 'cache-control': 'public, max-age=60' }));
app.get('/.well-known/oauth-protected-resource/mcp', c => c.json(mcpMetadata, 200, { 'cache-control': 'public, max-age=60' }));
installIdentityBrowserRoutes(app, runtime);
return installControlPlaneResourceRoutes(app, runtime.authenticate, options.registry, options.confirmations,
return installControlPlaneResourceRoutes(app, runtime.authenticate, registry, options.confirmations,
options.mcpBusForPrincipal, metadata.resource);
}
13 changes: 9 additions & 4 deletions src/api/control-plane/catalog/sdk-standard-schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { controlPlaneSchemaJson, type ControlPlaneOperationBinding } from '@treeseed/sdk/operator-contracts';
import { z } from 'zod';
import { CONFIRMATION_HEADER } from '../confirmation/confirmation-service.ts';

type SdkSchema = ControlPlaneOperationBinding<unknown, unknown, unknown, unknown>['schema']['output'];

Expand Down Expand Up @@ -32,22 +33,26 @@ export function sdkStandardSchema(schema: SdkSchema) {
}

export function sdkOperationInputStandardSchema(binding: {
descriptor: Pick<ControlPlaneOperationBinding['descriptor'], 'kind' | 'idempotency' | 'concurrency'>;
descriptor: Pick<ControlPlaneOperationBinding['descriptor'], 'kind' | 'idempotency' | 'concurrency' | 'rest' | 'confirmation'>;
schema: { path: SdkSchema; query: SdkSchema; body: SdkSchema };
}) {
const acceptsBody = Boolean(binding.descriptor.rest && binding.descriptor.rest.method !== 'GET');
const headerNames: string[] = binding.descriptor.kind === 'mutation'
? [binding.descriptor.idempotency.header, ...(binding.descriptor.concurrency.required ? [binding.descriptor.concurrency.writeHeader] : [])] : [];
const headersSchema = headerNames.length ? z.object(Object.fromEntries(headerNames.map(name => [name,
z.string().min(1).regex(/^(?=.*\S)[^\r\n\u0000]+$/u).describe(name === binding.descriptor.idempotency.header
? 'Reuse this key for retries of the unchanged request.' : 'Exact current resource version read before this mutation.'),
]))).strict() : undefined;
]))).extend(binding.descriptor.confirmation === 'input_required' ? {
[CONFIRMATION_HEADER]: z.string().min(1).regex(/^(?=.*\S)[^\r\n\u0000]+$/u)
.describe('Encoded signed confirmation returned by a previous call with these exact arguments; supply only when approval is authorized.').optional(),
} : {}).strict() : undefined;
const document = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
path: mcpSchemaJson(binding.schema.path),
query: mcpSchemaJson(binding.schema.query),
...(binding.descriptor.kind === 'mutation' ? { body: mcpSchemaJson(binding.schema.body) } : {}),
...(acceptsBody ? { body: mcpSchemaJson(binding.schema.body) } : {}),
...(headersSchema ? { headers: z.toJSONSchema(headersSchema, { target: 'draft-7' }) } : {}),
},
additionalProperties: false,
Expand All @@ -69,7 +74,7 @@ export function sdkOperationInputStandardSchema(binding: {
}
const path = binding.schema.path.safeParse(input.path ?? {});
const query = binding.schema.query.safeParse(input.query ?? {});
const body = binding.schema.body.safeParse(binding.descriptor.kind === 'mutation' ? input.body ?? {} : undefined);
const body = binding.schema.body.safeParse(acceptsBody ? input.body ?? {} : undefined);
if (!path.success) return { issues: issues(path.error) };
if (!query.success) return { issues: issues(query.error) };
if (!body.success) return { issues: issues(body.error) };
Expand Down
2 changes: 2 additions & 0 deletions src/api/control-plane/confirmation/confirmation-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import type { ConfirmationState, InputRequired } from '@treeseed/sdk/operator-contracts';

export const CONFIRMATION_HEADER = 'x-treeseed-confirmation';

interface ConfirmationClaims {
schemaVersion: 'treeseed.confirmation-state/v1';
principalId: string;
Expand Down
4 changes: 2 additions & 2 deletions src/api/control-plane/http/operation-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Context } from 'hono';
import { ZodError } from 'zod';
import { ControlPlaneOperationError, type BoundOperation } from '../catalog/operation-registry.ts';
import { enforceOperationAuthorization, readOperationPreconditions } from '../authorization/invocation-policy.ts';
import { decodeConfirmation, type ConfirmationService } from '../confirmation/confirmation-service.ts';
import { CONFIRMATION_HEADER, decodeConfirmation, type ConfirmationService } from '../confirmation/confirmation-service.ts';

type Authenticate = (request: Request) => Promise<AuthInfo | Response>;

Expand Down Expand Up @@ -90,7 +90,7 @@ export function createOperationHttpHandler(
}
const identity = { principalId: String((authInfo.extra.principal as any).id), clientId: authInfo.clientId,
operationId: descriptor.operationId, arguments: input };
const supplied = decodeConfirmation(context.req.header('x-treeseed-confirmation'));
const supplied = decodeConfirmation(context.req.header(CONFIRMATION_HEADER));
if (!supplied) {
const required = confirmations.request({ ...identity, requestId });
return context.json({ type: 'https://treeseed.dev/problems/confirmation_required', title: 'Confirmation required',
Expand Down
14 changes: 9 additions & 5 deletions src/api/control-plane/mcp/create-mcp-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from 'zod';
import type { OperationRegistry } from '../catalog/operation-registry.ts';
import { sdkOperationInputStandardSchema, sdkStandardSchema } from '../catalog/sdk-standard-schema.ts';
import { createMcpCatalog } from './mcp-catalog.ts';
import { decodeConfirmation, encodeConfirmation, type ConfirmationService } from '../confirmation/confirmation-service.ts';
import { CONFIRMATION_HEADER, decodeConfirmation, encodeConfirmation, type ConfirmationService } from '../confirmation/confirmation-service.ts';
import { completePromptObjective, completeResourceVariable } from './completion-source.ts';
import { enforceOperationAuthorization, readOperationPreconditions } from '../authorization/invocation-policy.ts';

Expand Down Expand Up @@ -34,11 +34,15 @@ export function createControlPlaneMcpHandler(registry: OperationRegistry, confir
const principalId = String((requestContext.authInfo?.extra?.principal as any)?.id ?? '');
const clientId = String(requestContext.authInfo?.clientId ?? '');
if (!confirmations || !principalId || !clientId) return { content: [{ type: 'text', text: 'Confirmation is unavailable.' }], isError: true };
const identity = { principalId, clientId, operationId: descriptor.operationId, arguments: input };
const requestState = context.mcpReq.requestState<string>();
const { [CONFIRMATION_HEADER]: supplied, ...operationHeaders } = headers ?? {};
const identity = { principalId, clientId, operationId: descriptor.operationId,
arguments: headers ? { ...operationInput, headers: operationHeaders } : operationInput };
const requestState = supplied ?? context.mcpReq.requestState<string>();
const accepted = acceptedContent<{ confirm: boolean }>(context.mcpReq.inputResponses, 'confirm');
if (!requestState || accepted?.confirm !== true) {
if (!requestState || (supplied === undefined && accepted?.confirm !== true)) {
const required = confirmations.request({ ...identity, requestId: String(context.mcpReq.id) });
if (requestContext.era === 'legacy') return { isError: true,
content: [{ type: 'text', text: JSON.stringify(required) }] };
return inputRequired({ requestState: encodeConfirmation(required.confirmation), inputRequests: {
confirm: inputRequired.elicit({ message: required.prompt, requestedSchema: {
type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'], additionalProperties: false,
Expand Down Expand Up @@ -99,5 +103,5 @@ export function createControlPlaneMcpHandler(registry: OperationRegistry, confir
}));
}
return server;
}, { legacy: 'reject', responseMode: 'auto', bus });
}, { legacy: 'stateless', responseMode: 'auto', bus });
}
Loading
Loading