@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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/capabilities.d.ts +19 -0
- package/dist/commands/capabilities.d.ts.map +1 -0
- package/dist/commands/capabilities.js +128 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin-mode/plugin-operation-contract.d.ts +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +4 -0
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +2 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +3 -2
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.d.ts +1 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.js +4 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +2 -2
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +3 -1
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/authoring/dist/index.js +1 -0
- package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
- package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +2 -1
- package/node_modules/@noodle-borg/authoring/dist/recording.js +29 -1
- package/node_modules/@noodle-borg/authoring/dist/server.d.ts +3 -0
- package/node_modules/@noodle-borg/authoring/dist/server.js +5 -1
- package/node_modules/@noodle-borg/authoring/dist/web-capabilities.d.ts +17 -0
- package/node_modules/@noodle-borg/authoring/dist/web-capabilities.js +15 -0
- package/node_modules/@noodle-borg/authoring/package.json +1 -0
- package/node_modules/@noodle-borg/capabilities/dist/index.js +7 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-observability.js +102 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +3 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +2 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +2 -1
- package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +2 -1
- package/node_modules/@noodle-borg/compiler/dist/compile.js +2 -0
- package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js +18 -3
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +34 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +9 -2
- package/node_modules/@noodle-borg/compiler/dist/web-capabilities.js +55 -0
- package/node_modules/@noodle-borg/compiler/package.json +1 -0
- package/node_modules/@noodle-borg/connector-http/dist/ssrf.js +5 -0
- package/node_modules/@noodle-borg/knowledge-crawl/dist/bounded-body.js +38 -0
- package/node_modules/@noodle-borg/knowledge-crawl/dist/first-party.js +2 -3
- package/node_modules/@noodle-borg/knowledge-crawl/dist/html-text.js +10 -2
- package/node_modules/@noodle-borg/knowledge-crawl/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/knowledge-crawl/dist/index.js +1 -0
- package/node_modules/@noodle-borg/knowledge-crawl/dist/page-reader.d.ts +26 -0
- package/node_modules/@noodle-borg/knowledge-crawl/dist/page-reader.js +127 -0
- package/node_modules/@noodle-borg/knowledge-crawl/dist/robots.js +48 -23
- package/node_modules/@noodle-borg/knowledge-crawl/package.json +1 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/budget.d.ts +29 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/budget.js +124 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/contracts.d.ts +212 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/contracts.js +113 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/errors.d.ts +8 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/errors.js +21 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/executor.d.ts +26 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/executor.js +156 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/limits.d.ts +15 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/limits.js +15 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/operator-contracts.d.ts +159 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/operator-contracts.js +69 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/operator-route.d.ts +5 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/operator-route.js +160 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/policy-store.d.ts +34 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/policy-store.js +61 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/portable.d.ts +10 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/portable.js +10 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/service.d.ts +83 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/service.js +139 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/urls.d.ts +3 -0
- package/node_modules/@noodle-borg/managed-capabilities/dist/urls.js +30 -0
- package/node_modules/@noodle-borg/managed-capabilities/package.json +49 -0
- package/node_modules/@noodle-borg/module/dist/contract.d.ts +11 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation.js +29 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +2 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +3 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.js +6 -1
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts +2 -1
- package/node_modules/@noodle-borg/runtime/dist/index.js +2 -1
- package/node_modules/@noodle-borg/runtime/dist/interactive.js +10 -1
- package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +3 -0
- package/node_modules/@noodle-borg/runtime/dist/result-meta.d.ts +2 -0
- package/node_modules/@noodle-borg/runtime/dist/result-meta.js +6 -0
- package/node_modules/@noodle-borg/runtime/dist/result.d.ts +4 -0
- package/node_modules/@noodle-borg/runtime/dist/web-capabilities.d.ts +71 -0
- package/node_modules/@noodle-borg/runtime/dist/web-capabilities.js +41 -0
- package/node_modules/@noodle-borg/runtime/package.json +1 -0
- package/node_modules/@noodle-borg/service/dist/local-options.d.ts +1 -0
- package/node_modules/@noodle-borg/service/dist/local-web-capabilities.js +13 -0
- package/node_modules/@noodle-borg/service/dist/mcp-protocol-runtime.js +21 -0
- package/node_modules/@noodle-borg/service/dist/modules/route-dispatch.js +13 -0
- package/node_modules/@noodle-borg/service/dist/registry-compile.js +3 -0
- package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +1 -0
- package/node_modules/@noodle-borg/service/dist/registry.js +3 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +5 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +7 -4
- package/node_modules/@noodle-borg/service/dist/serve-local.js +14 -0
- package/node_modules/@noodle-borg/service/dist/serve-resource-auth.js +10 -0
- package/node_modules/@noodle-borg/service/dist/service.js +7 -1
- package/node_modules/@noodle-borg/service/dist/web-capabilities.js +39 -0
- package/node_modules/@noodle-borg/service/package.json +3 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
- package/node_modules/@noodle-borg/wire-contracts/package.json +1 -0
- 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,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
|
}
|