@noodleseed/one 0.169.0 → 0.170.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +3 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/capabilities.d.ts +19 -0
  5. package/dist/commands/capabilities.d.ts.map +1 -0
  6. package/dist/commands/capabilities.js +128 -0
  7. package/dist/commands/capabilities.js.map +1 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/plugin-mode/plugin-operation-contract.d.ts +2 -2
  13. package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +4 -0
  14. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  15. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +2 -0
  16. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +3 -2
  17. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  18. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.d.ts +1 -0
  19. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.js +4 -1
  20. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +2 -2
  21. package/node_modules/@noodle-borg/authoring/dist/assistant.js +3 -1
  22. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -0
  23. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -0
  24. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  25. package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +2 -1
  26. package/node_modules/@noodle-borg/authoring/dist/recording.js +29 -1
  27. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
  28. package/node_modules/@noodle-borg/authoring/dist/server.js +5 -1
  29. package/node_modules/@noodle-borg/authoring/dist/web-capabilities.d.ts +17 -0
  30. package/node_modules/@noodle-borg/authoring/dist/web-capabilities.js +15 -0
  31. package/node_modules/@noodle-borg/authoring/package.json +1 -0
  32. package/node_modules/@noodle-borg/capabilities/dist/index.js +7 -0
  33. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-observability.js +102 -0
  34. package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +3 -0
  35. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +2 -0
  36. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
  37. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
  38. package/node_modules/@noodle-borg/compiler/dist/compile.js +2 -0
  39. package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js +18 -3
  40. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +34 -0
  41. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +9 -2
  42. package/node_modules/@noodle-borg/compiler/dist/web-capabilities.js +55 -0
  43. package/node_modules/@noodle-borg/compiler/package.json +1 -0
  44. package/node_modules/@noodle-borg/connector-http/dist/ssrf.js +5 -0
  45. package/node_modules/@noodle-borg/knowledge-crawl/dist/bounded-body.js +38 -0
  46. package/node_modules/@noodle-borg/knowledge-crawl/dist/first-party.js +2 -3
  47. package/node_modules/@noodle-borg/knowledge-crawl/dist/html-text.js +10 -2
  48. package/node_modules/@noodle-borg/knowledge-crawl/dist/index.d.ts +1 -0
  49. package/node_modules/@noodle-borg/knowledge-crawl/dist/index.js +1 -0
  50. package/node_modules/@noodle-borg/knowledge-crawl/dist/page-reader.d.ts +26 -0
  51. package/node_modules/@noodle-borg/knowledge-crawl/dist/page-reader.js +127 -0
  52. package/node_modules/@noodle-borg/knowledge-crawl/dist/robots.js +48 -23
  53. package/node_modules/@noodle-borg/knowledge-crawl/package.json +1 -0
  54. package/node_modules/@noodle-borg/managed-capabilities/dist/budget.d.ts +29 -0
  55. package/node_modules/@noodle-borg/managed-capabilities/dist/budget.js +124 -0
  56. package/node_modules/@noodle-borg/managed-capabilities/dist/contracts.d.ts +212 -0
  57. package/node_modules/@noodle-borg/managed-capabilities/dist/contracts.js +113 -0
  58. package/node_modules/@noodle-borg/managed-capabilities/dist/errors.d.ts +8 -0
  59. package/node_modules/@noodle-borg/managed-capabilities/dist/errors.js +21 -0
  60. package/node_modules/@noodle-borg/managed-capabilities/dist/executor.d.ts +26 -0
  61. package/node_modules/@noodle-borg/managed-capabilities/dist/executor.js +156 -0
  62. package/node_modules/@noodle-borg/managed-capabilities/dist/limits.d.ts +15 -0
  63. package/node_modules/@noodle-borg/managed-capabilities/dist/limits.js +15 -0
  64. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-contracts.d.ts +159 -0
  65. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-contracts.js +69 -0
  66. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-route.d.ts +5 -0
  67. package/node_modules/@noodle-borg/managed-capabilities/dist/operator-route.js +160 -0
  68. package/node_modules/@noodle-borg/managed-capabilities/dist/policy-store.d.ts +34 -0
  69. package/node_modules/@noodle-borg/managed-capabilities/dist/policy-store.js +61 -0
  70. package/node_modules/@noodle-borg/managed-capabilities/dist/portable.d.ts +10 -0
  71. package/node_modules/@noodle-borg/managed-capabilities/dist/portable.js +10 -0
  72. package/node_modules/@noodle-borg/managed-capabilities/dist/service.d.ts +83 -0
  73. package/node_modules/@noodle-borg/managed-capabilities/dist/service.js +139 -0
  74. package/node_modules/@noodle-borg/managed-capabilities/dist/urls.d.ts +3 -0
  75. package/node_modules/@noodle-borg/managed-capabilities/dist/urls.js +30 -0
  76. package/node_modules/@noodle-borg/managed-capabilities/package.json +49 -0
  77. package/node_modules/@noodle-borg/module/dist/contract.d.ts +11 -0
  78. package/node_modules/@noodle-borg/runtime/dist/confirmation.js +29 -0
  79. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +2 -0
  80. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +3 -0
  81. package/node_modules/@noodle-borg/runtime/dist/execute.js +6 -1
  82. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +2 -1
  83. package/node_modules/@noodle-borg/runtime/dist/index.js +2 -1
  84. package/node_modules/@noodle-borg/runtime/dist/interactive.js +10 -1
  85. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +3 -0
  86. package/node_modules/@noodle-borg/runtime/dist/result-meta.d.ts +2 -0
  87. package/node_modules/@noodle-borg/runtime/dist/result-meta.js +6 -0
  88. package/node_modules/@noodle-borg/runtime/dist/result.d.ts +4 -0
  89. package/node_modules/@noodle-borg/runtime/dist/web-capabilities.d.ts +71 -0
  90. package/node_modules/@noodle-borg/runtime/dist/web-capabilities.js +41 -0
  91. package/node_modules/@noodle-borg/runtime/package.json +1 -0
  92. package/node_modules/@noodle-borg/service/dist/local-options.d.ts +1 -0
  93. package/node_modules/@noodle-borg/service/dist/local-web-capabilities.js +13 -0
  94. package/node_modules/@noodle-borg/service/dist/mcp-protocol-runtime.js +21 -0
  95. package/node_modules/@noodle-borg/service/dist/modules/route-dispatch.js +13 -0
  96. package/node_modules/@noodle-borg/service/dist/registry-compile.js +3 -0
  97. package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +1 -0
  98. package/node_modules/@noodle-borg/service/dist/registry.js +3 -0
  99. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +5 -2
  100. package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +7 -4
  101. package/node_modules/@noodle-borg/service/dist/serve-local.js +14 -0
  102. package/node_modules/@noodle-borg/service/dist/serve-resource-auth.js +10 -0
  103. package/node_modules/@noodle-borg/service/dist/service.js +7 -1
  104. package/node_modules/@noodle-borg/service/dist/web-capabilities.js +39 -0
  105. package/node_modules/@noodle-borg/service/package.json +3 -1
  106. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  107. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  108. package/node_modules/@noodle-borg/wire-contracts/package.json +1 -0
  109. package/package.json +3 -2
@@ -0,0 +1,160 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { CapabilityBudget } from './budget.js';
4
+ import { webCapabilityBaseSchema } from './contracts.js';
5
+ import { CapabilityError } from './errors.js';
6
+ import { capabilityInspectionSchema, capabilityListResponseSchema, capabilityPolicyUpdateSchema, capabilityScopeSchema, capabilityTestRequestSchema, capabilityTestResponseSchema, } from './operator-contracts.js';
7
+ import { CapabilityPolicyConflict } from './policy-store.js';
8
+ const path = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/capabilities(?:\/([^/]+)(?:\/(policy|test))?)?$/;
9
+ /** Operator diagnostic commands share policy/admission; they never expose source bodies. */
10
+ export function capabilityOperatorRoute(service) {
11
+ return {
12
+ id: 'managed-capabilities',
13
+ match: (_method, url) => path.test(url.pathname),
14
+ handle: async (req, res, ctx) => {
15
+ try {
16
+ const match = path.exec(new URL(req.url ?? '/', 'http://service.invalid').pathname);
17
+ if (!match)
18
+ return send(res, 404, { error: 'not_found' });
19
+ const scope = capabilityScopeSchema.parse({
20
+ org: match[1],
21
+ app: match[2],
22
+ env: match[3],
23
+ name: match[4] ?? 'list',
24
+ });
25
+ const { name, ...tenant } = scope;
26
+ const action = match[5];
27
+ const reading = req.method === 'GET' && action === undefined;
28
+ if (!reading &&
29
+ !(req.method === 'PUT' && action === 'policy') &&
30
+ !(req.method === 'POST' && action === 'test')) {
31
+ return send(res, 405, { error: 'method_not_allowed' });
32
+ }
33
+ const auth = await ctx.tenantControl?.authorize(req, {
34
+ org: scope.org,
35
+ permission: reading ? 'org:member' : 'org:manage',
36
+ });
37
+ if (auth?.ok !== true || auth.identity === undefined)
38
+ return send(res, auth?.ok === false ? auth.status : 401, {
39
+ error: 'capability_forbidden',
40
+ });
41
+ const target = await ctx.capabilityDeployments?.get(req, tenant);
42
+ if (target === undefined)
43
+ return send(res, 404, { error: 'deployment_not_found' });
44
+ // The service has already compiled and authorized these declarations. Diagnostics need only intent.
45
+ const declarations = target.declarations.map((raw) => webCapabilityBaseSchema.strip().parse(raw));
46
+ if (match[4] === undefined) {
47
+ return send(res, 200, capabilityListResponseSchema.parse({
48
+ deploymentId: target.deploymentId,
49
+ capabilities: await Promise.all(declarations.map((d) => service.inspect(tenant, d))),
50
+ }));
51
+ }
52
+ const declaration = declarations.find((d) => d.name === name);
53
+ if (declaration === undefined)
54
+ return send(res, 404, { error: 'capability_not_found' });
55
+ if (reading)
56
+ return send(res, 200, capabilityInspectionSchema.parse(await service.inspect(tenant, declaration)));
57
+ const body = await jsonBody(req);
58
+ if (action === 'policy') {
59
+ const update = capabilityPolicyUpdateSchema.parse(body);
60
+ const record = await service.configure(tenant, declaration, {
61
+ ...update,
62
+ actor: auth.identity.subject,
63
+ });
64
+ await ctx.audit?.emit({
65
+ eventType: 'capability.policy.updated',
66
+ ...tenant,
67
+ actorSubject: auth.identity.subject,
68
+ deploymentId: target.deploymentId,
69
+ details: { name, revision: record.revision, enabled: record.policy.enabled },
70
+ });
71
+ return send(res, 200, capabilityInspectionSchema.parse(await service.inspect(tenant, declaration)));
72
+ }
73
+ const test = capabilityTestRequestSchema.parse(body);
74
+ const cancelled = new AbortController();
75
+ const abort = () => cancelled.abort();
76
+ req.once('aborted', abort);
77
+ res.once('close', abort);
78
+ try {
79
+ const result = await service.execute(declaration, test.request, {
80
+ tenant,
81
+ deploymentId: target.deploymentId,
82
+ executionId: randomUUID(),
83
+ subject: auth.identity.subject,
84
+ anonymous: false,
85
+ authorized: true,
86
+ budget: new CapabilityBudget(),
87
+ signal: cancelled.signal,
88
+ }, test.mode === 'fixture'
89
+ ? {
90
+ read: async ({ url }) => ({
91
+ url,
92
+ title: 'Diagnostic fixture',
93
+ text: 'Synthetic diagnostic evidence. No website was fetched.',
94
+ links: [],
95
+ retrievedAt: new Date().toISOString(),
96
+ }),
97
+ }
98
+ : undefined);
99
+ return send(res, 200, capabilityTestResponseSchema.parse({
100
+ mode: test.mode,
101
+ status: result.status,
102
+ pages: result.items.length,
103
+ sources: result.sources.map(({ ref, url, retrievedAt }) => ({
104
+ ref,
105
+ url,
106
+ retrievedAt,
107
+ })),
108
+ warnings: result.warnings,
109
+ }));
110
+ }
111
+ finally {
112
+ req.off('aborted', abort);
113
+ res.off('close', abort);
114
+ }
115
+ }
116
+ catch (error) {
117
+ const status = error instanceof CapabilityPolicyConflict
118
+ ? 409
119
+ : error instanceof z.ZodError
120
+ ? 400
121
+ : error instanceof CapabilityError
122
+ ? 422
123
+ : 503;
124
+ send(res, status, {
125
+ error: error instanceof CapabilityPolicyConflict
126
+ ? 'capability_policy_conflict'
127
+ : error instanceof z.ZodError
128
+ ? 'invalid_request'
129
+ : error instanceof CapabilityError
130
+ ? error.code
131
+ : 'capability_unavailable',
132
+ });
133
+ }
134
+ },
135
+ };
136
+ }
137
+ async function jsonBody(req) {
138
+ const chunks = [];
139
+ let size = 0;
140
+ for await (const raw of req) {
141
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
142
+ size += chunk.length;
143
+ if (size > 16_384)
144
+ throw new CapabilityError('capability_source_rejected');
145
+ chunks.push(chunk);
146
+ }
147
+ try {
148
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
149
+ }
150
+ catch {
151
+ throw new CapabilityError('capability_source_rejected');
152
+ }
153
+ }
154
+ function send(res, status, value) {
155
+ if (res.destroyed || res.writableEnded)
156
+ return;
157
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
158
+ res.end(JSON.stringify(value));
159
+ }
160
+ //# sourceMappingURL=operator-route.js.map
@@ -0,0 +1,34 @@
1
+ import { type CapabilityPolicyRecord, type CapabilityPolicyUpdate, type CapabilityScope } from './operator-contracts.js';
2
+ export * from './operator-contracts.js';
3
+ export interface CapabilityPolicyStore {
4
+ readonly durable: boolean;
5
+ get(scope: CapabilityScope): Promise<CapabilityPolicyRecord | undefined>;
6
+ replace(scope: CapabilityScope, update: CapabilityPolicyUpdate): Promise<CapabilityPolicyRecord>;
7
+ }
8
+ export declare class CapabilityPolicyConflict extends Error {
9
+ constructor();
10
+ }
11
+ export declare function capabilityScopeKey(scope: CapabilityScope): string;
12
+ export declare function checkedPolicyUpdate(update: CapabilityPolicyUpdate): {
13
+ actor: string;
14
+ fingerprint: string;
15
+ expectedRevision: number;
16
+ mutationId: string;
17
+ policy: {
18
+ enabled: boolean;
19
+ dailyCalls: number;
20
+ maxUrls?: number | undefined;
21
+ maxCalls?: number | undefined;
22
+ timeoutMs?: number | undefined;
23
+ maxTextBytes?: number | undefined;
24
+ domains?: string[] | undefined;
25
+ };
26
+ };
27
+ /** Development/test only. Hosted composition rejects a non-durable policy or counter store. */
28
+ export declare class InMemoryCapabilityPolicyStore implements CapabilityPolicyStore {
29
+ #private;
30
+ readonly durable = false;
31
+ get(scope: CapabilityScope): Promise<CapabilityPolicyRecord | undefined>;
32
+ replace(scope: CapabilityScope, input: CapabilityPolicyUpdate): Promise<CapabilityPolicyRecord>;
33
+ }
34
+ //# sourceMappingURL=policy-store.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { capabilityPolicyUpdateSchema, capabilityScopeSchema, } from './operator-contracts.js';
4
+ export * from './operator-contracts.js';
5
+ export class CapabilityPolicyConflict extends Error {
6
+ constructor() {
7
+ super('capability_policy_conflict');
8
+ }
9
+ }
10
+ export function capabilityScopeKey(scope) {
11
+ const parsed = capabilityScopeSchema.parse(scope);
12
+ return JSON.stringify([
13
+ parsed.org,
14
+ parsed.app,
15
+ parsed.env,
16
+ `capability.web-extract.${parsed.name}.provider`,
17
+ ]);
18
+ }
19
+ export function checkedPolicyUpdate(update) {
20
+ const { actor, ...body } = update;
21
+ const parsed = capabilityPolicyUpdateSchema.parse(body);
22
+ z.string().min(1).max(512).parse(actor);
23
+ const fingerprint = createHash('sha256')
24
+ .update(JSON.stringify({ ...parsed, actor }))
25
+ .digest('hex');
26
+ return { ...parsed, actor, fingerprint };
27
+ }
28
+ /** Development/test only. Hosted composition rejects a non-durable policy or counter store. */
29
+ export class InMemoryCapabilityPolicyStore {
30
+ durable = false;
31
+ #records = new Map();
32
+ async get(scope) {
33
+ const entry = this.#records.get(capabilityScopeKey(scope));
34
+ return entry === undefined ? undefined : structuredClone(entry.record);
35
+ }
36
+ async replace(scope, input) {
37
+ const key = capabilityScopeKey(scope);
38
+ const update = checkedPolicyUpdate(input);
39
+ const prior = this.#records.get(key);
40
+ if (prior?.mutationId === update.mutationId) {
41
+ if (prior.fingerprint !== update.fingerprint)
42
+ throw new CapabilityPolicyConflict();
43
+ return structuredClone(prior.record);
44
+ }
45
+ if ((prior?.record.revision ?? 0) !== update.expectedRevision)
46
+ throw new CapabilityPolicyConflict();
47
+ const record = {
48
+ revision: update.expectedRevision + 1,
49
+ policy: update.policy,
50
+ actor: update.actor,
51
+ updatedAt: new Date().toISOString(),
52
+ };
53
+ this.#records.set(key, {
54
+ record,
55
+ mutationId: update.mutationId,
56
+ fingerprint: update.fingerprint,
57
+ });
58
+ return structuredClone(record);
59
+ }
60
+ }
61
+ //# sourceMappingURL=policy-store.js.map
@@ -0,0 +1,10 @@
1
+ export { CapabilityBudget, type CapabilityBudgetSnapshot } from './budget.js';
2
+ export * from './contracts.js';
3
+ export { CAPABILITY_ERROR_CODES, CapabilityError, type CapabilityErrorCode } from './errors.js';
4
+ export { effectiveWebPolicy, executeWebExtract, type PublicPageReaderPort, type WebExtractDeps, } from './executor.js';
5
+ export { WEB_EXTRACT_LIMITS } from './limits.js';
6
+ export { capabilityOperatorRoute } from './operator-route.js';
7
+ export * from './policy-store.js';
8
+ export { type CapabilityInvocation, CapabilityService, type CapabilityServiceOptions, } from './service.js';
9
+ export { publicPageUrl } from './urls.js';
10
+ //# sourceMappingURL=portable.d.ts.map
@@ -0,0 +1,10 @@
1
+ export { CapabilityBudget } from './budget.js';
2
+ export * from './contracts.js';
3
+ export { CAPABILITY_ERROR_CODES, CapabilityError } from './errors.js';
4
+ export { effectiveWebPolicy, executeWebExtract, } from './executor.js';
5
+ export { WEB_EXTRACT_LIMITS } from './limits.js';
6
+ export { capabilityOperatorRoute } from './operator-route.js';
7
+ export * from './policy-store.js';
8
+ export { CapabilityService, } from './service.js';
9
+ export { publicPageUrl } from './urls.js';
10
+ //# sourceMappingURL=portable.js.map
@@ -0,0 +1,83 @@
1
+ import type { AtomicDailyCounterStore } from '@noodle-borg/admission-limits/portable';
2
+ import type { CapabilityBudget } from './budget.js';
3
+ import type { WebCapability, WebExtractResult } from './contracts.js';
4
+ import { type PublicPageReaderPort } from './executor.js';
5
+ import { type CapabilityPolicyStore, type CapabilityPolicyUpdate, type CapabilityScope } from './policy-store.js';
6
+ type Tenant = Pick<CapabilityScope, 'org' | 'app' | 'env'>;
7
+ export interface CapabilityServiceOptions {
8
+ readonly profile: 'development' | 'hosted';
9
+ readonly policies: CapabilityPolicyStore;
10
+ readonly counters: AtomicDailyCounterStore;
11
+ readonly reader: PublicPageReaderPort;
12
+ /** Exact hosted cohort targets. Empty or absent disables all hosted extraction. */
13
+ readonly enabledScopes?: readonly Tenant[];
14
+ readonly audit?: (fields: Readonly<Record<string, string | number | boolean>> & Tenant) => void | Promise<void>;
15
+ }
16
+ export interface CapabilityInvocation {
17
+ readonly tenant: Tenant;
18
+ readonly deploymentId: string;
19
+ readonly executionId: string;
20
+ readonly subject?: string;
21
+ readonly anonymous: boolean;
22
+ readonly network?: string;
23
+ readonly authorized: boolean;
24
+ readonly budget: CapabilityBudget;
25
+ readonly signal?: AbortSignal;
26
+ }
27
+ /** Policy/admission authority shared by normal tools and operator diagnostics. Never resolves a model. */
28
+ export declare class CapabilityService {
29
+ readonly options: CapabilityServiceOptions;
30
+ constructor(options: CapabilityServiceOptions);
31
+ enabled(tenant: Tenant): boolean;
32
+ inspect(tenant: Tenant, declaration: WebCapability): Promise<{
33
+ actor?: string;
34
+ updatedAt?: string;
35
+ name: string;
36
+ class: "web.extract.v1";
37
+ title: string;
38
+ serviceKey: string;
39
+ available: boolean;
40
+ profile: string;
41
+ revision: number;
42
+ developerPolicy: {
43
+ maxUrls?: number | undefined;
44
+ maxCalls?: number | undefined;
45
+ timeoutMs?: number | undefined;
46
+ maxTextBytes?: number | undefined;
47
+ domains?: string[] | undefined;
48
+ };
49
+ operatorPolicy: {
50
+ enabled: boolean;
51
+ dailyCalls: number;
52
+ maxUrls?: number | undefined;
53
+ maxCalls?: number | undefined;
54
+ timeoutMs?: number | undefined;
55
+ maxTextBytes?: number | undefined;
56
+ domains?: string[] | undefined;
57
+ };
58
+ effectivePolicy: {
59
+ maxUrls?: number | undefined;
60
+ maxCalls?: number | undefined;
61
+ timeoutMs?: number | undefined;
62
+ maxTextBytes?: number | undefined;
63
+ domains?: string[] | undefined;
64
+ };
65
+ }>;
66
+ configure(tenant: Tenant, declaration: WebCapability, update: CapabilityPolicyUpdate): Promise<{
67
+ revision: number;
68
+ policy: {
69
+ enabled: boolean;
70
+ dailyCalls: number;
71
+ maxUrls?: number | undefined;
72
+ maxCalls?: number | undefined;
73
+ timeoutMs?: number | undefined;
74
+ maxTextBytes?: number | undefined;
75
+ domains?: string[] | undefined;
76
+ };
77
+ actor: string;
78
+ updatedAt: string;
79
+ }>;
80
+ execute(declaration: WebCapability, input: unknown, context: CapabilityInvocation, reader?: PublicPageReaderPort): Promise<WebExtractResult>;
81
+ }
82
+ export {};
83
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1,139 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { CapabilityError } from './errors.js';
3
+ import { effectiveWebPolicy, executeWebExtract } from './executor.js';
4
+ import { WEB_EXTRACT_LIMITS } from './limits.js';
5
+ import { capabilityScopeKey, } from './policy-store.js';
6
+ /** Policy/admission authority shared by normal tools and operator diagnostics. Never resolves a model. */
7
+ export class CapabilityService {
8
+ options;
9
+ constructor(options) {
10
+ this.options = options;
11
+ if (options.profile === 'hosted' && (!options.policies.durable || !options.counters.durable)) {
12
+ throw new Error('capability_durable_authority_required');
13
+ }
14
+ }
15
+ enabled(tenant) {
16
+ return (this.options.profile === 'development' ||
17
+ (this.options.enabledScopes ?? []).some((scope) => scope.org === tenant.org && scope.app === tenant.app && scope.env === tenant.env));
18
+ }
19
+ async inspect(tenant, declaration) {
20
+ const record = await this.options.policies.get({ ...tenant, name: declaration.name });
21
+ const operator = record === undefined ? {} : policyBounds(record.policy);
22
+ return {
23
+ name: declaration.name,
24
+ class: declaration.class,
25
+ title: declaration.title,
26
+ serviceKey: `capability.web-extract.${declaration.name}.provider`,
27
+ available: this.enabled(tenant) && record?.policy.enabled === true,
28
+ profile: this.options.profile === 'hosted' ? 'managed-learning-cohort' : 'local-first-party',
29
+ revision: record?.revision ?? 0,
30
+ developerPolicy: declaration.policy ?? {},
31
+ operatorPolicy: record?.policy ?? { enabled: false, dailyCalls: 100 },
32
+ effectivePolicy: effectiveWebPolicy(declaration.policy, operator),
33
+ ...(record === undefined ? {} : { actor: record.actor, updatedAt: record.updatedAt }),
34
+ };
35
+ }
36
+ async configure(tenant, declaration, update) {
37
+ const effective = effectiveWebPolicy(declaration.policy, policyBounds(update.policy));
38
+ for (const key of ['maxUrls', 'maxCalls', 'timeoutMs', 'maxTextBytes']) {
39
+ if (update.policy[key] !== undefined && update.policy[key] !== effective[key])
40
+ throw new CapabilityError('capability_policy_denied');
41
+ }
42
+ if (update.policy.domains?.some((host) => !effective.domains?.includes(host)))
43
+ throw new CapabilityError('capability_policy_denied');
44
+ return this.options.policies.replace({ ...tenant, name: declaration.name }, update);
45
+ }
46
+ async execute(declaration, input, context, reader = this.options.reader) {
47
+ const scope = { ...context.tenant, name: declaration.name };
48
+ if (!this.enabled(context.tenant))
49
+ throw new CapabilityError('capability_unavailable');
50
+ let record;
51
+ try {
52
+ record = await this.options.policies.get(scope);
53
+ }
54
+ catch {
55
+ throw new CapabilityError('capability_unavailable');
56
+ }
57
+ if (record?.policy.enabled !== true)
58
+ throw new CapabilityError('capability_unavailable');
59
+ if (context.anonymous && !context.network)
60
+ throw new CapabilityError('capability_policy_denied');
61
+ if (!context.anonymous && !context.subject)
62
+ throw new CapabilityError('capability_policy_denied');
63
+ const scopeId = opaque(capabilityScopeKey(scope));
64
+ const amount = WEB_EXTRACT_LIMITS.maxHttpAttempts;
65
+ const attribution = context.anonymous
66
+ ? `network:${context.network}`
67
+ : `subject:${context.subject}`;
68
+ const started = Date.now();
69
+ try {
70
+ const result = await executeWebExtract(declaration, input, {
71
+ reader,
72
+ enabled: true,
73
+ authorized: context.authorized,
74
+ budget: context.budget,
75
+ operatorPolicy: policyBounds(record.policy),
76
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
77
+ admit: async () => {
78
+ const requests = [
79
+ { key: 'capability:web-extract:fleet', amount, limit: 1000 * amount },
80
+ { key: `capability:${scopeId}`, amount, limit: record.policy.dailyCalls * amount },
81
+ {
82
+ key: `capability:caller:${opaque(`${scopeId}:${attribution}`)}`,
83
+ amount,
84
+ limit: 10 * amount,
85
+ },
86
+ ...(context.network === undefined
87
+ ? []
88
+ : [
89
+ {
90
+ key: `capability:network:${opaque(context.network)}`,
91
+ amount,
92
+ limit: 30 * amount,
93
+ },
94
+ ]),
95
+ ];
96
+ const attempt = {
97
+ key: `capability:attempt:${opaque(`${scopeId}:${context.deploymentId}:${context.executionId}`)}`,
98
+ fingerprint: opaque(JSON.stringify(input)),
99
+ };
100
+ const outcome = await this.options.counters.consumeAllOnce(requests, attempt, new Date());
101
+ // A replay is not a cache hit: there is no retained source body, so it cannot authorize more I/O.
102
+ return outcome.kind === 'consumed';
103
+ },
104
+ });
105
+ await this.options.audit?.({
106
+ ...context.tenant,
107
+ class: declaration.class,
108
+ scope: scopeId,
109
+ policyRevision: record.revision,
110
+ status: result.status,
111
+ pages: result.items.length,
112
+ elapsedMs: Date.now() - started,
113
+ });
114
+ return result;
115
+ }
116
+ catch (error) {
117
+ const safe = error instanceof CapabilityError ? error : new CapabilityError('capability_unavailable');
118
+ await Promise.resolve()
119
+ .then(() => this.options.audit?.({
120
+ ...context.tenant,
121
+ class: declaration.class,
122
+ scope: scopeId,
123
+ policyRevision: record.revision,
124
+ status: safe.code,
125
+ elapsedMs: Date.now() - started,
126
+ }))
127
+ .catch(() => { });
128
+ throw safe;
129
+ }
130
+ }
131
+ }
132
+ function opaque(value) {
133
+ return createHash('sha256').update(value).digest('hex');
134
+ }
135
+ function policyBounds(policy) {
136
+ const { enabled: _enabled, dailyCalls: _daily, ...bounds } = policy;
137
+ return bounds;
138
+ }
139
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1,3 @@
1
+ /** Syntactic policy only. The adapter must additionally pin socket DNS to public unicast addresses. */
2
+ export declare function publicPageUrl(value: string, domains?: readonly string[]): URL;
3
+ //# sourceMappingURL=urls.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { isIP } from 'node:net';
2
+ import { CapabilityError } from './errors.js';
3
+ import { WEB_EXTRACT_LIMITS } from './limits.js';
4
+ /** Syntactic policy only. The adapter must additionally pin socket DNS to public unicast addresses. */
5
+ export function publicPageUrl(value, domains) {
6
+ let url;
7
+ try {
8
+ url = new URL(value);
9
+ }
10
+ catch {
11
+ throw new CapabilityError('capability_source_rejected');
12
+ }
13
+ const host = url.hostname;
14
+ if (value.length > WEB_EXTRACT_LIMITS.maxUrlCharacters ||
15
+ url.protocol !== 'https:' ||
16
+ url.username !== '' ||
17
+ url.password !== '' ||
18
+ url.port !== '' ||
19
+ url.search !== '' ||
20
+ isIP(host.replace(/^\[|\]$/g, '')) !== 0 ||
21
+ !host.includes('.') ||
22
+ host.endsWith('.') ||
23
+ /(?:^|\.)(?:localhost|local|internal|test|invalid)$/.test(host) ||
24
+ (domains !== undefined && !domains.includes(host))) {
25
+ throw new CapabilityError('capability_source_rejected');
26
+ }
27
+ url.hash = '';
28
+ return url;
29
+ }
30
+ //# sourceMappingURL=urls.js.map
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@noodle-borg/managed-capabilities",
3
+ "version": "0.0.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
9
+ "main": "./dist/portable.js",
10
+ "types": "./dist/portable.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/portable.d.ts",
14
+ "import": "./dist/portable.js"
15
+ },
16
+ "./portable": {
17
+ "types": "./dist/portable.d.ts",
18
+ "import": "./dist/portable.js"
19
+ },
20
+ "./contracts": {
21
+ "types": "./dist/operator-contracts.d.ts",
22
+ "import": "./dist/operator-contracts.js"
23
+ },
24
+ "./testing": {
25
+ "types": "./dist/conformance.d.ts",
26
+ "import": "./dist/conformance.js"
27
+ },
28
+ "./postgres": {
29
+ "types": "./dist/postgres.d.ts",
30
+ "import": "./dist/postgres.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit"
39
+ },
40
+ "dependencies": {
41
+ "@noodle-borg/module": "0.0.0",
42
+ "@noodle-borg/admission-limits": "0.0.0",
43
+ "zod": "^4.4.3"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.13.1",
47
+ "@types/pg": "^8.20.0"
48
+ }
49
+ }
@@ -25,6 +25,17 @@ export interface ModuleRouteContext {
25
25
  readonly tenantControl?: TenantControlAuthorizer;
26
26
  /** Narrow deployment/App Package projection for hosted package operations. */
27
27
  readonly deploymentPackages?: DeploymentPackageView;
28
+ /** Read-only exact active capability declarations, available after same-request tenant authorization. */
29
+ readonly capabilityDeployments?: {
30
+ get(req: IncomingMessage, scope: {
31
+ readonly org: string;
32
+ readonly app: string;
33
+ readonly env: string;
34
+ }): Promise<{
35
+ readonly deploymentId: string;
36
+ readonly declarations: readonly unknown[];
37
+ } | undefined>;
38
+ };
28
39
  /** Minimal live deployment/package identity used only after an opaque delivery lookup. */
29
40
  readonly distributionDelivery?: DistributionDeliveryView;
30
41
  /** Host audit sink for route-emitted audit events; absent means the host has no audit trail. */
@@ -1,3 +1,4 @@
1
+ import { CapabilityBudget } from '@noodle-borg/managed-capabilities';
1
2
  import { resolveVariableEnvironment, validateVariableContinuation } from './business-variables.js';
2
3
  import { analyzeEligibleActions, countTrailingEligibleOperations, isActionOperation, validateConfirmationFlow, } from './confirmation-analysis.js';
3
4
  import { artifactIdentity, confirmationRequired, sameArtifact, } from './confirmation-continuation.js';
@@ -10,12 +11,32 @@ import { createHost, evalExprMap, prepareOperationAction, resolveEnv, runOperati
10
11
  import { AllowAllPolicy } from './policy/allow-all.js';
11
12
  import { attachResultMeta, splitResultMeta } from './result-meta.js';
12
13
  import { admitToolDispatch, latchToolDispatchAdmission } from './tool-dispatch.js';
14
+ function carryBudget(result, budget) {
15
+ if (result.status === 'input_required')
16
+ return {
17
+ ...result,
18
+ continuation: { ...result.continuation, capabilityBudget: budget.snapshot() },
19
+ };
20
+ if (result.status === 'confirmation_required')
21
+ return {
22
+ ...result,
23
+ continuation: { ...result.continuation, capabilityBudget: budget.snapshot() },
24
+ };
25
+ return result;
26
+ }
13
27
  /**
14
28
  * Evaluate the pure prefix of a confirmable tool. Connector operations are an absolute suspension
15
29
  * boundary: this function either requests input, returns a prepared confirmation, or fails before
16
30
  * invoking one.
17
31
  */
18
32
  export async function prepareToolForConfirmation(artifact, toolName, input, deps) {
33
+ const capabilityBudget = deps.capabilityBudget ?? new CapabilityBudget();
34
+ return carryBudget(await prepareToolForConfirmationInternal(artifact, toolName, input, {
35
+ ...deps,
36
+ capabilityBudget,
37
+ }), capabilityBudget);
38
+ }
39
+ async function prepareToolForConfirmationInternal(artifact, toolName, input, deps) {
19
40
  if (artifact.resolution !== 'resolved') {
20
41
  return failPreparation('shape_only_artifact', 'runtime refuses to serve a shape-only artifact');
21
42
  }
@@ -52,6 +73,13 @@ export async function prepareToolForConfirmation(artifact, toolName, input, deps
52
73
  }
53
74
  /** Resume the pure preparation prefix with one accepted, declined, or cancelled input response. */
54
75
  export async function resumeToolPreparation(artifact, continuation, response, deps) {
76
+ const capabilityBudget = new CapabilityBudget(continuation.capabilityBudget);
77
+ return carryBudget(await resumeToolPreparationInternal(artifact, continuation, response, {
78
+ ...deps,
79
+ capabilityBudget,
80
+ }), capabilityBudget);
81
+ }
82
+ async function resumeToolPreparationInternal(artifact, continuation, response, deps) {
55
83
  if (artifact.resolution !== 'resolved') {
56
84
  return failPreparation('shape_only_artifact', 'runtime refuses to serve a shape-only artifact');
57
85
  }
@@ -94,6 +122,7 @@ export async function resumeToolPreparation(artifact, continuation, response, de
94
122
  * after performing a side effect.
95
123
  */
96
124
  export async function executePreparedTool(artifact, continuation, deps) {
125
+ deps = { ...deps, capabilityBudget: new CapabilityBudget(continuation.capabilityBudget) };
97
126
  if (artifact.resolution !== 'resolved') {
98
127
  return failInteractive('shape_only_artifact', 'runtime refuses to serve a shape-only artifact');
99
128
  }