@noodleseed/one 0.175.1 → 0.177.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 (72) hide show
  1. package/dist/commands/knowledge.d.ts.map +1 -1
  2. package/dist/commands/knowledge.js +4 -7
  3. package/dist/commands/knowledge.js.map +1 -1
  4. package/dist/commands/solutions-conversations.d.ts +6 -0
  5. package/dist/commands/solutions-conversations.d.ts.map +1 -0
  6. package/dist/commands/solutions-conversations.js +137 -0
  7. package/dist/commands/solutions-conversations.js.map +1 -0
  8. package/dist/commands/solutions-ops.d.ts.map +1 -1
  9. package/dist/commands/solutions-ops.js +3 -0
  10. package/dist/commands/solutions-ops.js.map +1 -1
  11. package/dist/commands/whatsapp-ops.d.ts.map +1 -1
  12. package/dist/commands/whatsapp-ops.js +12 -2
  13. package/dist/commands/whatsapp-ops.js.map +1 -1
  14. package/dist/commands/whatsapp-output.d.ts.map +1 -1
  15. package/dist/commands/whatsapp-output.js +1 -0
  16. package/dist/commands/whatsapp-output.js.map +1 -1
  17. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +14 -8
  18. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +11 -2
  19. package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +14 -5
  20. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +5 -3
  21. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-conversations.js +97 -0
  22. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +2 -0
  23. package/node_modules/@noodle-borg/knowledge-operations/dist/executor.d.ts +3 -5
  24. package/node_modules/@noodle-borg/knowledge-operations/dist/executor.js +2 -7
  25. package/node_modules/@noodle-borg/knowledge-operations/dist/portable.d.ts +1 -1
  26. package/node_modules/@noodle-borg/knowledge-operations/dist/portable.js +1 -1
  27. package/node_modules/@noodle-borg/knowledge-operations/dist/publication.d.ts +0 -1
  28. package/node_modules/@noodle-borg/knowledge-operations/dist/publication.js +1 -10
  29. package/node_modules/@noodle-borg/knowledge-operations/dist/routes.d.ts +0 -4
  30. package/node_modules/@noodle-borg/knowledge-operations/dist/routes.js +1 -17
  31. package/node_modules/@noodle-borg/knowledge-operations/dist/search-port.d.ts +1 -2
  32. package/node_modules/@noodle-borg/knowledge-operations/dist/search-port.js +1 -3
  33. package/node_modules/@noodle-borg/knowledge-operations/dist/service-wiring.d.ts +4 -4
  34. package/node_modules/@noodle-borg/knowledge-operations/dist/service-wiring.js +3 -8
  35. package/node_modules/@noodle-borg/knowledge-operations/dist/status-routes.d.ts +1 -2
  36. package/node_modules/@noodle-borg/knowledge-operations/dist/status-routes.js +0 -14
  37. package/node_modules/@noodle-borg/protocol/dist/knowledge-tool.js +3 -5
  38. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +2 -3
  39. package/node_modules/@noodle-borg/service/dist/assistant-composition.js +9 -0
  40. package/node_modules/@noodle-borg/service/dist/business-information/validation.js +1 -22
  41. package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +41 -145
  42. package/node_modules/@noodle-borg/service/dist/channels/composition.js +1 -0
  43. package/node_modules/@noodle-borg/service/dist/channels/meta-cloud.js +102 -0
  44. package/node_modules/@noodle-borg/service/dist/channels/provider.js +187 -0
  45. package/node_modules/@noodle-borg/service/dist/channels/runtime.js +49 -21
  46. package/node_modules/@noodle-borg/service/dist/conversation-history/capture.js +99 -0
  47. package/node_modules/@noodle-borg/service/dist/conversation-history/contracts.js +17 -0
  48. package/node_modules/@noodle-borg/service/dist/conversation-history/operator.js +184 -0
  49. package/node_modules/@noodle-borg/service/dist/payment-card.js +34 -0
  50. package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +2 -0
  51. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-stream.js +4 -2
  52. package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +15 -0
  53. package/node_modules/@noodle-borg/service/dist/routes/assistant-knowledge.js +1 -3
  54. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +8 -6
  55. package/node_modules/@noodle-borg/service/dist/routes/business-information-conversations.js +65 -0
  56. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +3 -0
  57. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +16 -0
  58. package/node_modules/@noodle-borg/service/dist/routes/whatsapp-meta.js +130 -0
  59. package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +18 -9
  60. package/node_modules/@noodle-borg/service/dist/service-history-projections.js +24 -0
  61. package/node_modules/@noodle-borg/service/dist/service.js +3 -7
  62. package/node_modules/@noodle-borg/transport-http/dist/index.d.ts +1 -1
  63. package/node_modules/@noodle-borg/transport-http/dist/index.js +1 -1
  64. package/node_modules/@noodle-borg/transport-http/dist/request-body.d.ts +9 -0
  65. package/node_modules/@noodle-borg/transport-http/dist/request-body.js +2 -1
  66. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.d.ts +347 -0
  67. package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.js +120 -0
  68. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  69. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  70. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +31 -9
  71. package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +27 -4
  72. package/package.json +1 -1
@@ -65,7 +65,15 @@ export class ChannelCoordinator {
65
65
  };
66
66
  });
67
67
  }
68
+ /** The binding that owns a provider phone-number asset, whatever its provider or state. */
69
+ async bindingForAsset(phoneNumberId) {
70
+ return this.store.transaction([CHANNEL_REGISTRY], (tx) => channelValue(tx, CHANNEL_REGISTRY, `asset:${channelDigest(phoneNumberId)}`));
71
+ }
68
72
  async configure(input, actor, key, revision) {
73
+ const meta = input.provider === 'meta';
74
+ // 360dialog authenticates callbacks per binding; Meta signs them with the app secret per WABA.
75
+ if (meta ? !input.wabaId || input.webhookSecret : !input.webhookSecret || input.wabaId)
76
+ throw new ChannelError('provider_configuration_invalid');
69
77
  const tenantKey = channelTenantKey(input.tenant);
70
78
  const priorId = await this.store.transaction([CHANNEL_REGISTRY], (tx) => channelValue(tx, CHANNEL_REGISTRY, tenantKey));
71
79
  const prior = priorId ? await this.internal(priorId) : undefined;
@@ -86,10 +94,11 @@ export class ChannelCoordinator {
86
94
  if (owner && owner !== id)
87
95
  throw new ChannelError('asset_in_use');
88
96
  const now = this.now();
97
+ const { provider: _provider, ...configured } = input;
89
98
  const binding = {
90
- ...input,
99
+ ...configured,
91
100
  id,
92
- provider: '360dialog',
101
+ provider: meta ? 'meta' : '360dialog',
93
102
  revision: revision + 1,
94
103
  generation: (existing?.generation ?? 0) + 1,
95
104
  state: 'paused',
@@ -21,6 +21,8 @@ export type ChannelLimits = {
21
21
  export type ChannelLimitsInput = {
22
22
  [K in keyof ChannelLimits]?: number | undefined;
23
23
  };
24
+ /** `360dialog` relays the Cloud API with a per-binding callback secret; `meta` is the Cloud API itself. */
25
+ export type ChannelProvider = '360dialog' | 'meta';
24
26
  export type ChannelCapability = {
25
27
  readonly kind: 'tool' | 'knowledge';
26
28
  readonly name: string;
@@ -28,10 +30,14 @@ export type ChannelCapability = {
28
30
  export interface ChannelBinding {
29
31
  readonly id: string;
30
32
  readonly tenant: TenantRef;
31
- readonly provider: '360dialog';
33
+ readonly provider: ChannelProvider;
32
34
  readonly phoneNumberId: string;
35
+ /** The WhatsApp Business Account that owns the number; present only for `meta`. */
36
+ readonly wabaId?: string | undefined;
37
+ /** Tenant secret name of the provider credential: 360dialog key or Meta business token. */
33
38
  readonly apiKeySecret: string;
34
- readonly webhookSecret: string;
39
+ /** Tenant secret name of the per-binding callback secret; present only for `360dialog`. */
40
+ readonly webhookSecret?: string | undefined;
35
41
  readonly deploymentId: string;
36
42
  readonly capabilities: readonly ChannelCapability[];
37
43
  readonly supportEmail: string;
@@ -48,7 +54,9 @@ export interface ChannelBinding {
48
54
  readonly readyRevision?: number | undefined;
49
55
  readonly readyAt?: number | undefined;
50
56
  }
51
- export type ChannelConfigure = Pick<ChannelBinding, 'tenant' | 'phoneNumberId' | 'apiKeySecret' | 'webhookSecret' | 'deploymentId' | 'capabilities' | 'supportEmail'> & {
57
+ export type ChannelConfigure = Pick<ChannelBinding, 'tenant' | 'phoneNumberId' | 'wabaId' | 'apiKeySecret' | 'webhookSecret' | 'deploymentId' | 'capabilities' | 'supportEmail'> & {
58
+ /** Defaults to `360dialog`. */
59
+ readonly provider?: ChannelProvider | undefined;
52
60
  readonly limits?: ChannelLimitsInput | undefined;
53
61
  };
54
62
  export interface ChannelAddress {
@@ -126,10 +134,11 @@ export declare function writeChannelEvent(tx: ChannelTransaction, scope: string,
126
134
  export declare function publicChannelBinding(binding: ChannelBinding): {
127
135
  id: string;
128
136
  tenant: TenantRef;
129
- provider: "360dialog";
137
+ provider: ChannelProvider;
130
138
  phoneNumberId: string;
139
+ wabaId?: string | undefined;
131
140
  apiKeySecret: string;
132
- webhookSecret: string;
141
+ webhookSecret?: string | undefined;
133
142
  deploymentId: string;
134
143
  capabilities: readonly ChannelCapability[];
135
144
  supportEmail: string;
@@ -48,14 +48,16 @@ export const CHANNELS_COMMAND = {
48
48
  arguments: [],
49
49
  flags: [],
50
50
  subcommands: [
51
- group('whatsapp', 'Configure the 360dialog channel, test readiness, and control public messaging.', [
51
+ group('whatsapp', 'Configure a 360dialog or Meta Cloud API number, test readiness, and control public messaging.', [
52
52
  leaf('status', 'Show configuration and current revision.'),
53
53
  leaf('configure', 'Save a paused binding using managed secret references.', [
54
54
  revision,
55
55
  ...mutation,
56
+ flag('provider', '360dialog (default) or meta for the Meta Cloud API.'),
56
57
  flag('phone-number-id', '360dialog/Meta phone-number asset ID.'),
57
- flag('api-key-secret', 'Managed secret name for the 360dialog key.'),
58
- flag('webhook-secret', 'Independent managed callback secret name.'),
58
+ flag('waba-id', 'WhatsApp Business Account ID; required with --provider meta.'),
59
+ flag('api-key-secret', 'Managed secret name for the 360dialog key or the Meta business token.'),
60
+ flag('webhook-secret', 'Independent managed callback secret name; 360dialog only.'),
59
61
  flag('capabilities', 'Comma-separated knowledge:name or tool:name selections.'),
60
62
  flag('support-email', 'Contact shown when an answer cannot be completed.'),
61
63
  ...limits,
@@ -0,0 +1,97 @@
1
+ /** Staff conversation history operations (ADR 0241); imported by the solutions family. Data only. */
2
+ import { INSTALLATION_ARGUMENT, OPTIONAL_FLAG, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
3
+ const paging = (maximum) => [
4
+ {
5
+ ...OPTIONAL_FLAG,
6
+ name: 'limit',
7
+ type: 'integer',
8
+ value: '<count>',
9
+ summary: 'Maximum conversations in this page.',
10
+ constraints: { minimum: 1, maximum },
11
+ },
12
+ {
13
+ ...OPTIONAL_FLAG,
14
+ name: 'cursor',
15
+ type: 'string',
16
+ value: '<cursor>',
17
+ summary: 'Opaque continuation from the prior page.',
18
+ },
19
+ {
20
+ ...OPTIONAL_FLAG,
21
+ name: 'channel',
22
+ type: 'string',
23
+ value: '<channel>',
24
+ summary: 'Only conversations from this channel.',
25
+ constraints: { choices: ['website', 'whatsapp'] },
26
+ },
27
+ ];
28
+ const SELECTORS = ['conversation', 'customer', 'participant'];
29
+ const SELECTOR_SUMMARIES = {
30
+ conversation: 'Conversation id to erase.',
31
+ customer: 'Verified customer reference whose conversations are erased.',
32
+ participant: 'WhatsApp participant reference whose conversations are erased.',
33
+ };
34
+ export const SOLUTION_CONVERSATIONS = {
35
+ name: 'conversations',
36
+ summary: 'Read, export and forget customer conversation history.',
37
+ arguments: [],
38
+ flags: [],
39
+ subcommands: [
40
+ {
41
+ name: 'list',
42
+ summary: 'List conversations with unexpired messages, newest first.',
43
+ arguments: [INSTALLATION_ARGUMENT],
44
+ flags: [...SOLUTION_COMMON_FLAGS, ...paging(100)],
45
+ jsonOutput: { mode: 'single' },
46
+ },
47
+ {
48
+ name: 'show',
49
+ summary: 'Show one conversation with its messages; the read is audited.',
50
+ arguments: [INSTALLATION_ARGUMENT],
51
+ flags: [
52
+ ...SOLUTION_COMMON_FLAGS,
53
+ {
54
+ ...OPTIONAL_FLAG,
55
+ required: true,
56
+ name: 'conversation',
57
+ type: 'string',
58
+ value: '<id>',
59
+ summary: 'Conversation id from conversations list.',
60
+ },
61
+ ],
62
+ jsonOutput: { mode: 'single' },
63
+ },
64
+ {
65
+ name: 'export',
66
+ summary: 'Export one page of conversations with messages (Owner or Administrator).',
67
+ arguments: [INSTALLATION_ARGUMENT],
68
+ flags: [...SOLUTION_COMMON_FLAGS, ...paging(25)],
69
+ jsonOutput: { mode: 'single' },
70
+ },
71
+ {
72
+ name: 'forget',
73
+ summary: 'Erase one conversation, or every conversation of a customer or participant.',
74
+ arguments: [INSTALLATION_ARGUMENT],
75
+ flags: [
76
+ ...SOLUTION_COMMON_FLAGS,
77
+ ...SELECTORS.map((name) => ({
78
+ ...OPTIONAL_FLAG,
79
+ name,
80
+ type: 'string',
81
+ value: name === 'conversation' ? '<id>' : '<ref>',
82
+ summary: SELECTOR_SUMMARIES[name],
83
+ conflictsWith: SELECTORS.filter((other) => other !== name),
84
+ })),
85
+ {
86
+ ...OPTIONAL_FLAG,
87
+ name: 'confirm',
88
+ type: 'boolean',
89
+ required: true,
90
+ summary: 'Confirm permanently erasing the selected conversation history.',
91
+ },
92
+ ],
93
+ jsonOutput: { mode: 'single' },
94
+ },
95
+ ],
96
+ };
97
+ //# sourceMappingURL=catalog-data-solution-conversations.js.map
@@ -1,4 +1,5 @@
1
1
  /** Solution installation/operator catalog. Imports only dependency-free sibling data. */
2
+ import { SOLUTION_CONVERSATIONS } from './catalog-data-solution-conversations.js';
2
3
  import { SOLUTION_DRAFTS } from './catalog-data-solution-drafts.js';
3
4
  import { AUTH_TOKEN, EXPECTED_REVISION, IDEMPOTENCY_KEY, INSTALLATION_ARGUMENT, JSON_FLAG, OPTIONAL_FLAG, PAGING_FLAGS, REQUIRED_ARGUMENT, SERVICE, SOLUTION_COMMON_FLAGS, } from './catalog-data-solution-flags.js';
4
5
  import { SOLUTION_PAGE } from './catalog-data-solution-page.js';
@@ -171,6 +172,7 @@ export const CATALOG_SOLUTIONS = {
171
172
  },
172
173
  ],
173
174
  },
175
+ SOLUTION_CONVERSATIONS,
174
176
  {
175
177
  name: 'activity',
176
178
  summary: 'Inspect payload-free operation evidence and its retention policy.',
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Runtime `search_<name>` execution (ADR 0202 amendment 2026-08-18): feature gate → bundled
3
- * BM25 for documents → bundled BM25 over the crawled site corpus → deterministic RRF fusion with
2
+ * Runtime `search_<name>` execution (ADR 0202 amendment 2026-08-18): bundled BM25 for documents
3
+ * → bundled BM25 over the crawled site corpus → deterministic RRF fusion with
4
4
  * the exact origin/path policy postfilter. Every query is in-process and free; the crawl-page
5
5
  * budget is enforced at crawl time (`crawl-lifecycle.ts`), not here. Before a component's first
6
6
  * completed crawl the site corpus is honestly empty, with the crawl state as the operator
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { type SearchHit } from '@noodle-borg/knowledge/portable';
10
10
  import type { KnowledgeDeployHooks } from './publication.js';
11
- import { type KnowledgeTenantRef } from './routes.js';
11
+ import type { KnowledgeTenantRef } from './routes.js';
12
12
  /** The component facts the executor needs; a structural subset of the compiled component. */
13
13
  export interface KnowledgeSearchComponent {
14
14
  readonly name: string;
@@ -22,13 +22,11 @@ export interface KnowledgeSearchRequest {
22
22
  readonly limit?: number;
23
23
  }
24
24
  export interface KnowledgeSearchExecutor {
25
- enabled(tenant: KnowledgeTenantRef): Promise<boolean>;
26
25
  /** Throws `KnowledgeError` or `SearchBudgetExhaustedError`; never returns partial truth. */
27
26
  search(tenant: KnowledgeTenantRef, component: KnowledgeSearchComponent, request: KnowledgeSearchRequest): Promise<readonly SearchHit[]>;
28
27
  }
29
28
  export interface KnowledgeSearchExecutorDeps {
30
29
  readonly hooks: KnowledgeDeployHooks;
31
- readonly knowledgeEnabled: (tenant: KnowledgeTenantRef) => Promise<boolean>;
32
30
  }
33
31
  export declare function createKnowledgeSearchExecutor(deps: KnowledgeSearchExecutorDeps): KnowledgeSearchExecutor;
34
32
  //# sourceMappingURL=executor.d.ts.map
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Runtime `search_<name>` execution (ADR 0202 amendment 2026-08-18): feature gate → bundled
3
- * BM25 for documents → bundled BM25 over the crawled site corpus → deterministic RRF fusion with
2
+ * Runtime `search_<name>` execution (ADR 0202 amendment 2026-08-18): bundled BM25 for documents
3
+ * → bundled BM25 over the crawled site corpus → deterministic RRF fusion with
4
4
  * the exact origin/path policy postfilter. Every query is in-process and free; the crawl-page
5
5
  * budget is enforced at crawl time (`crawl-lifecycle.ts`), not here. Before a component's first
6
6
  * completed crawl the site corpus is honestly empty, with the crawl state as the operator
@@ -8,14 +8,9 @@
8
8
  */
9
9
  import { DEFAULT_RESULT_LIMIT, fuseHits, KnowledgeError, MAX_QUERY_CHARS, MAX_RESULT_LIMIT, MIN_QUERY_CHARS, MIN_RESULT_LIMIT, } from '@noodle-borg/knowledge/portable';
10
10
  import { searchSiteCorpus } from './crawl-lifecycle.js';
11
- import { knowledgeEnableCommand } from './routes.js';
12
11
  export function createKnowledgeSearchExecutor(deps) {
13
12
  return {
14
- enabled: (tenant) => deps.knowledgeEnabled(tenant),
15
13
  async search(tenant, component, request) {
16
- if (!(await deps.knowledgeEnabled(tenant))) {
17
- throw new KnowledgeError('request', `knowledge is not enabled for this org/app/env; run: ${knowledgeEnableCommand(tenant)}`);
18
- }
19
14
  if (request.query.length < MIN_QUERY_CHARS || request.query.length > MAX_QUERY_CHARS) {
20
15
  throw new KnowledgeError('request', `query must be between ${MIN_QUERY_CHARS} and ${MAX_QUERY_CHARS} characters`);
21
16
  }
@@ -5,7 +5,7 @@ export { createKnowledgeSearchExecutor, type KnowledgeSearchComponent, type Know
5
5
  export { readBody, sendJson } from './http.js';
6
6
  export { type KnowledgeRouteRef, parseKnowledgePath } from './paths.js';
7
7
  export { createKnowledgeDeployHooks, type KnowledgeDeployFailure, type KnowledgeDeployHooks, KnowledgePublicationError, type KnowledgePublicationPlan, withKnowledgePublication, } from './publication.js';
8
- export { handleKnowledgeDocumentUpload, handleKnowledgePreflight, type KnowledgeRouteDeps, type KnowledgeTenantRef, knowledgeEnableCommand, knowledgeTenantKey, } from './routes.js';
8
+ export { handleKnowledgeDocumentUpload, handleKnowledgePreflight, type KnowledgeRouteDeps, type KnowledgeTenantRef, knowledgeTenantKey, } from './routes.js';
9
9
  export { type BoundKnowledgeSearchPort, bindKnowledgeSearchPort, type KnowledgeSearchPortFactory, knowledgeSearchPortFactory, } from './search-port.js';
10
10
  export { budgetCeilingsResolver, buildKnowledgeRouteDeps, defaultKnowledgeStores, type KnowledgeServiceStores, wireKnowledge, } from './service-wiring.js';
11
11
  export { InMemoryKnowledgeStagingStore, type KnowledgeStagingStore, STAGING_TTL_MS, } from './staging-store.js';
@@ -5,7 +5,7 @@ export { createKnowledgeSearchExecutor, } from './executor.js';
5
5
  export { readBody, sendJson } from './http.js';
6
6
  export { parseKnowledgePath } from './paths.js';
7
7
  export { createKnowledgeDeployHooks, KnowledgePublicationError, withKnowledgePublication, } from './publication.js';
8
- export { handleKnowledgeDocumentUpload, handleKnowledgePreflight, knowledgeEnableCommand, knowledgeTenantKey, } from './routes.js';
8
+ export { handleKnowledgeDocumentUpload, handleKnowledgePreflight, knowledgeTenantKey, } from './routes.js';
9
9
  export { bindKnowledgeSearchPort, knowledgeSearchPortFactory, } from './search-port.js';
10
10
  export { budgetCeilingsResolver, buildKnowledgeRouteDeps, defaultKnowledgeStores, wireKnowledge, } from './service-wiring.js';
11
11
  export { InMemoryKnowledgeStagingStore, STAGING_TTL_MS, } from './staging-store.js';
@@ -51,7 +51,6 @@ export interface KnowledgeDeployHooks {
51
51
  sweep(): Promise<void>;
52
52
  }
53
53
  export declare function createKnowledgeDeployHooks(stores: KnowledgeServiceStores, options: {
54
- readonly knowledgeEnabled: (tenant: KnowledgeTenantRef) => Promise<boolean>;
55
54
  /** Managed site datastore existence; when bound, a site() deploy fails before activation. */
56
55
  readonly siteProvisioned?: (tenant: KnowledgeTenantRef) => Promise<boolean>;
57
56
  /**
@@ -5,7 +5,7 @@
5
5
  * the previous artifact + revision pair; retry is idempotent by content hash.
6
6
  */
7
7
  import { Bm25KnowledgeIndex, identityDocumentTextCodec, KnowledgeError, revisionContentHash, } from '@noodle-borg/knowledge/portable';
8
- import { knowledgeEnableCommand, knowledgeTenantKey } from './routes.js';
8
+ import { knowledgeTenantKey } from './routes.js';
9
9
  /** Structured publication failure the registry maps onto its deploy error shape. */
10
10
  export class KnowledgePublicationError extends Error {
11
11
  errors;
@@ -84,15 +84,6 @@ export function createKnowledgeDeployHooks(stores, options) {
84
84
  const hooks = {
85
85
  async publish(tenant, deploymentId, components) {
86
86
  const withDocuments = components.filter((component) => component.documents.length > 0);
87
- if (components.length > 0 && !(await options.knowledgeEnabled(tenant))) {
88
- throw new KnowledgePublicationError([
89
- {
90
- code: 'knowledge_not_enabled',
91
- path: 'knowledge',
92
- message: `knowledge is not enabled for this org/app/env; run: ${knowledgeEnableCommand(tenant)}`,
93
- },
94
- ]);
95
- }
96
87
  const withSites = components.filter((component) => component.sites.length > 0);
97
88
  if (withSites.length > 0 && options.siteProvisioned !== undefined) {
98
89
  if (!(await options.siteProvisioned(tenant))) {
@@ -8,8 +8,6 @@ export interface KnowledgeTenantRef {
8
8
  }
9
9
  export interface KnowledgeRouteDeps {
10
10
  readonly staging: KnowledgeStagingStore;
11
- /** Managed-config feature gate (`NOODLE_KNOWLEDGE_ENABLED`), resolved org → app → env. */
12
- readonly knowledgeEnabled: (tenant: KnowledgeTenantRef) => Promise<boolean>;
13
11
  readonly maxBodyBytes: number;
14
12
  /** Seals staged bytes at rest; the service injects its secret-box codec. */
15
13
  readonly codec?: DocumentTextCodec;
@@ -17,8 +15,6 @@ export interface KnowledgeRouteDeps {
17
15
  readonly status?: import('./status-routes.js').KnowledgeStatusDeps;
18
16
  }
19
17
  export declare function knowledgeTenantKey(tenant: KnowledgeTenantRef): string;
20
- /** The one command that turns the feature on for a tenant; every fail-closed error names it. */
21
- export declare function knowledgeEnableCommand(tenant: KnowledgeTenantRef): string;
22
18
  /**
23
19
  * `POST /v1/orgs/{o}/apps/{a}/envs/{e}/knowledge/preflight` — diff declared document hashes
24
20
  * against transient staging and published revisions; respond with exactly the hashes that
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Knowledge control-plane route handlers (ADR 0202 as amended). The service dispatch
3
3
  * authorizes the request and parses the tenant path, then delegates here; these handlers own
4
- * validation, the feature gate, staging, and wire shapes — never auth.
4
+ * validation, staging, and wire shapes — never auth.
5
5
  */
6
6
  import { createHash } from 'node:crypto';
7
7
  import { MAX_DOCUMENT_BYTES } from '@noodle-borg/knowledge/limits';
@@ -11,18 +11,6 @@ import { readBody, sendJson } from './http.js';
11
11
  export function knowledgeTenantKey(tenant) {
12
12
  return `${tenant.org}/${tenant.app}/${tenant.env}`;
13
13
  }
14
- /** The one command that turns the feature on for a tenant; every fail-closed error names it. */
15
- export function knowledgeEnableCommand(tenant) {
16
- return (`noodle variables set NOODLE_KNOWLEDGE_ENABLED --value true --runtime cloud ` +
17
- `--scope env --org ${tenant.org} --app ${tenant.app} --env ${tenant.env}`);
18
- }
19
- function sendGateClosed(res, tenant) {
20
- sendJson(res, 403, {
21
- code: 'knowledge_not_enabled',
22
- error: 'knowledge is not enabled for this org/app/env',
23
- fix: knowledgeEnableCommand(tenant),
24
- });
25
- }
26
14
  const SHA256_HEX = /^[0-9a-f]{64}$/;
27
15
  /**
28
16
  * `POST /v1/orgs/{o}/apps/{a}/envs/{e}/knowledge/preflight` — diff declared document hashes
@@ -30,8 +18,6 @@ const SHA256_HEX = /^[0-9a-f]{64}$/;
30
18
  * still need bytes. Descriptors only; no contents cross this route.
31
19
  */
32
20
  export async function handleKnowledgePreflight(req, res, tenant, deps) {
33
- if (!(await deps.knowledgeEnabled(tenant)))
34
- return sendGateClosed(res, tenant);
35
21
  const body = await readBody(req, deps.maxBodyBytes);
36
22
  if (!body.ok)
37
23
  return sendJson(res, body.status, { error: body.error });
@@ -74,8 +60,6 @@ export async function handleKnowledgePreflight(req, res, tenant, deps) {
74
60
  * the service verifies declared length and hash before publication).
75
61
  */
76
62
  export async function handleKnowledgeDocumentUpload(req, res, tenant, sha256, deps) {
77
- if (!(await deps.knowledgeEnabled(tenant)))
78
- return sendGateClosed(res, tenant);
79
63
  if (!SHA256_HEX.test(sha256)) {
80
64
  return sendJson(res, 400, {
81
65
  code: 'invalid_knowledge_request',
@@ -8,7 +8,6 @@ import { type SearchHit } from '@noodle-borg/knowledge/portable';
8
8
  import type { KnowledgeSearchComponent, KnowledgeSearchExecutor } from './executor.js';
9
9
  import type { KnowledgeTenantRef } from './routes.js';
10
10
  export interface BoundKnowledgeSearchPort {
11
- enabled(): Promise<boolean>;
12
11
  search(componentName: string, request: {
13
12
  readonly query: string;
14
13
  readonly limit?: number;
@@ -17,7 +16,7 @@ export interface BoundKnowledgeSearchPort {
17
16
  readonly hits: readonly SearchHit[];
18
17
  } | {
19
18
  readonly ok: false;
20
- readonly reason: 'budget_exhausted' | 'not_enabled' | 'provider_error';
19
+ readonly reason: 'budget_exhausted' | 'provider_error';
21
20
  readonly message: string;
22
21
  }>;
23
22
  }
@@ -10,7 +10,6 @@ export function knowledgeSearchPortFactory(executor) {
10
10
  }
11
11
  export function bindKnowledgeSearchPort(executor, tenant, components) {
12
12
  return {
13
- enabled: () => executor.enabled(tenant),
14
13
  async search(componentName, request) {
15
14
  const component = components.find((candidate) => candidate.name === componentName);
16
15
  if (component === undefined) {
@@ -25,8 +24,7 @@ export function bindKnowledgeSearchPort(executor, tenant, components) {
25
24
  return { ok: false, reason: 'budget_exhausted', message: error.message };
26
25
  }
27
26
  if (error instanceof KnowledgeError) {
28
- const reason = error.layer === 'request' ? 'not_enabled' : 'provider_error';
29
- return { ok: false, reason, message: error.message };
27
+ return { ok: false, reason: 'provider_error', message: error.message };
30
28
  }
31
29
  return { ok: false, reason: 'provider_error', message: 'knowledge search failed' };
32
30
  }
@@ -20,8 +20,8 @@ export interface KnowledgeServiceStores {
20
20
  readonly crawlState?: CrawlStateStore;
21
21
  /**
22
22
  * Composition seams for tests and BYO providers: fetcher selection defaults to the managed
23
- * first-party crawler; the deploy gate defaults to always-provisioned (the managed crawler
24
- * needs no provisioning — BYO configuration checks bind here).
23
+ * first-party crawler; the managed crawler needs no provisioning and BYO configuration checks
24
+ * bind here.
25
25
  */
26
26
  readonly fetcherFor?: (tenant: KnowledgeTenantRef, component: CrawlableComponent) => SiteFetcher;
27
27
  /** BYO index selection override; defaults to declaration-driven provider adapters. */
@@ -32,8 +32,8 @@ export interface KnowledgeServiceStores {
32
32
  export declare function defaultKnowledgeStores(): KnowledgeServiceStores;
33
33
  /** Budget ceilings from managed config with the ratified v0 defaults (2000 org / 1000 app). */
34
34
  export declare function budgetCeilingsResolver(resolveVariables: (tenant: KnowledgeTenantRef) => Promise<Record<string, string>>): (tenant: KnowledgeTenantRef) => Promise<SearchBudgetCeilings>;
35
- /** Assemble route deps over the service's managed-config resolver (`NOODLE_KNOWLEDGE_ENABLED`). */
36
- export declare function buildKnowledgeRouteDeps(stores: KnowledgeServiceStores, resolveVariables: (tenant: KnowledgeTenantRef) => Promise<Record<string, string>>, maxBodyBytes: number): KnowledgeRouteDeps;
35
+ /** Assemble the always-available knowledge route dependencies. */
36
+ export declare function buildKnowledgeRouteDeps(stores: KnowledgeServiceStores, maxBodyBytes: number): KnowledgeRouteDeps;
37
37
  /** Map a compiled index declaration plus resolved config values onto its provider adapter. */
38
38
  export declare function knowledgeIndexFromDeclaration(declaration: NonNullable<CompiledKnowledgeComponent['index']>, config: Readonly<Record<string, string>>, scope: KnowledgeScope): KnowledgeIndex;
39
39
  /**
@@ -32,11 +32,10 @@ export function budgetCeilingsResolver(resolveVariables) {
32
32
  };
33
33
  };
34
34
  }
35
- /** Assemble route deps over the service's managed-config resolver (`NOODLE_KNOWLEDGE_ENABLED`). */
36
- export function buildKnowledgeRouteDeps(stores, resolveVariables, maxBodyBytes) {
35
+ /** Assemble the always-available knowledge route dependencies. */
36
+ export function buildKnowledgeRouteDeps(stores, maxBodyBytes) {
37
37
  return {
38
38
  staging: stores.staging,
39
- knowledgeEnabled: async (tenant) => (await resolveVariables(tenant)).NOODLE_KNOWLEDGE_ENABLED === 'true',
40
39
  maxBodyBytes,
41
40
  ...(stores.codec === undefined ? {} : { codec: stores.codec }),
42
41
  };
@@ -61,7 +60,7 @@ export function knowledgeIndexFromDeclaration(declaration, config, scope) {
61
60
  */
62
61
  export function wireKnowledge(registry, stores, resolveVariables, maxBodyBytes, resolveSecrets = async () => ({})) {
63
62
  const resolved = stores ?? defaultKnowledgeStores();
64
- const deps = buildKnowledgeRouteDeps(resolved, resolveVariables, maxBodyBytes);
63
+ const deps = buildKnowledgeRouteDeps(resolved, maxBodyBytes);
65
64
  const ceilings = budgetCeilingsResolver(resolveVariables);
66
65
  // The managed first-party crawler needs no provisioning, so the deploy gate defaults open;
67
66
  // BYO provider configuration checks replace this seam when a component declares a provider.
@@ -117,7 +116,6 @@ export function wireKnowledge(registry, stores, resolveVariables, maxBodyBytes,
117
116
  return knowledgeIndexFromDeclaration(resolvedDeclaration, config, scope);
118
117
  };
119
118
  const hooks = createKnowledgeDeployHooks(resolved, {
120
- knowledgeEnabled: deps.knowledgeEnabled,
121
119
  siteProvisioned,
122
120
  indexFor: resolved.indexFor ?? defaultIndexFor,
123
121
  });
@@ -146,12 +144,10 @@ export function wireKnowledge(registry, stores, resolveVariables, maxBodyBytes,
146
144
  };
147
145
  const bareExecutor = createKnowledgeSearchExecutor({
148
146
  hooks,
149
- knowledgeEnabled: deps.knowledgeEnabled,
150
147
  });
151
148
  // Refresh-on-traffic: serving a sites component also checks whether its refresh is due and
152
149
  // kicks the crawl in the background — the visitor's answer never waits on it.
153
150
  const executor = {
154
- enabled: bareExecutor.enabled,
155
151
  search: async (tenant, component, request) => {
156
152
  const hits = await bareExecutor.search(tenant, component, request);
157
153
  if (component.sites.length > 0) {
@@ -182,7 +178,6 @@ export function wireKnowledge(registry, stores, resolveVariables, maxBodyBytes,
182
178
  registry.setKnowledgeDeployHooks(crawlingHooks, knowledgeSearchPortFactory(executor));
183
179
  const status = {
184
180
  revisionStore: resolved.revisionStore,
185
- knowledgeEnabled: deps.knowledgeEnabled,
186
181
  crawlState: (tenant, componentName) => crawlState.get(tenant, componentName),
187
182
  refresh: (tenant, component) => runComponentCrawl(crawlDeps, tenant, component),
188
183
  // Mirrors the fail-closed deploy gate above: without the managed project every site() is
@@ -7,10 +7,9 @@
7
7
  import type { IncomingMessage, ServerResponse } from 'node:http';
8
8
  import type { CompiledKnowledgeComponent, KnowledgeRevisionStore } from '@noodle-borg/knowledge/portable';
9
9
  import type { KnowledgeBudgetState, KnowledgeCrawlState } from '@noodle-borg/wire-contracts';
10
- import { type KnowledgeTenantRef } from './routes.js';
10
+ import type { KnowledgeTenantRef } from './routes.js';
11
11
  export interface KnowledgeStatusDeps {
12
12
  readonly revisionStore: KnowledgeRevisionStore;
13
- readonly knowledgeEnabled: (tenant: KnowledgeTenantRef) => Promise<boolean>;
14
13
  /** The active deployment's compiled knowledge components, or undefined when none is active. */
15
14
  readonly activeKnowledge: (tenant: KnowledgeTenantRef) => Promise<{
16
15
  readonly components: readonly CompiledKnowledgeComponent[];
@@ -1,12 +1,4 @@
1
1
  import { sendJson } from './http.js';
2
- import { knowledgeEnableCommand } from './routes.js';
3
- function gateClosed(res, tenant) {
4
- sendJson(res, 403, {
5
- code: 'knowledge_not_enabled',
6
- error: 'knowledge is not enabled for this org/app/env',
7
- fix: knowledgeEnableCommand(tenant),
8
- });
9
- }
10
2
  async function componentSummary(tenant, component, deploymentId, revisionStore) {
11
3
  const scope = { org: tenant.org, app: tenant.app, env: tenant.env };
12
4
  const active = component.documents.length > 0 ? await revisionStore.active(scope, component.name) : undefined;
@@ -22,8 +14,6 @@ async function componentSummary(tenant, component, deploymentId, revisionStore)
22
14
  }
23
15
  /** `GET /v1/orgs/{o}/apps/{a}/envs/{e}/knowledge` */
24
16
  export async function handleKnowledgeList(_req, res, tenant, deps) {
25
- if (!(await deps.knowledgeEnabled(tenant)))
26
- return gateClosed(res, tenant);
27
17
  const active = await deps.activeKnowledge(tenant);
28
18
  const components = [];
29
19
  for (const component of active?.components ?? []) {
@@ -38,8 +28,6 @@ export async function handleKnowledgeList(_req, res, tenant, deps) {
38
28
  }
39
29
  /** `GET /v1/orgs/{o}/apps/{a}/envs/{e}/knowledge/{name}/status` */
40
30
  export async function handleKnowledgeStatus(_req, res, tenant, componentName, deps) {
41
- if (!(await deps.knowledgeEnabled(tenant)))
42
- return gateClosed(res, tenant);
43
31
  const active = await deps.activeKnowledge(tenant);
44
32
  const component = active?.components.find((candidate) => candidate.name === componentName);
45
33
  if (active === undefined || component === undefined) {
@@ -70,8 +58,6 @@ export async function handleKnowledgeStatus(_req, res, tenant, componentName, de
70
58
  * The outcome (including failures) is the returned crawl state, never a thrown error.
71
59
  */
72
60
  export async function handleKnowledgeRefresh(_req, res, tenant, componentName, deps) {
73
- if (!(await deps.knowledgeEnabled(tenant)))
74
- return gateClosed(res, tenant);
75
61
  const active = await deps.activeKnowledge(tenant);
76
62
  const component = active?.components.find((candidate) => candidate.name === componentName);
77
63
  if (component === undefined || deps.refresh === undefined) {
@@ -2,20 +2,18 @@ import { coerceToolArguments } from './input-validation.js';
2
2
  export function findKnowledgeComponent(artifact, toolName) {
3
3
  return artifact.server.knowledge?.find((component) => component.generatedTool.name === toolName);
4
4
  }
5
- /** Listing predicate: the artifact declares knowledge AND the deployment's port is live. */
5
+ /** Listing predicate: the artifact declares knowledge and the deployment has a search port. */
6
6
  export async function knowledgeToolsEnabled(artifact, deps) {
7
7
  if ((artifact.server.knowledge?.length ?? 0) === 0)
8
8
  return false;
9
- if (deps.knowledgeSearch === undefined)
10
- return false;
11
- return deps.knowledgeSearch.enabled();
9
+ return deps.knowledgeSearch !== undefined;
12
10
  }
13
11
  export async function runKnowledgeSearchTool(component, deps, rawArguments) {
14
12
  const coerced = coerceToolArguments(component.generatedTool.inputSchema, rawArguments);
15
13
  if (coerced.issues.length > 0)
16
14
  return { kind: 'invalid', issues: coerced.issues };
17
15
  if (deps.knowledgeSearch === undefined) {
18
- return { kind: 'error', reason: 'not_enabled', message: 'knowledge search is unavailable' };
16
+ return { kind: 'error', reason: 'provider_error', message: 'knowledge search is unavailable' };
19
17
  }
20
18
  const request = coerced.value;
21
19
  const outcome = await deps.knowledgeSearch.search(component.name, request);
@@ -61,10 +61,9 @@ export interface KnowledgeSearchHit {
61
61
  }
62
62
  /**
63
63
  * Deployment-bound knowledge retrieval port (structural; the service adapts its executor).
64
- * `enabled` gates listing as well as calls — a disabled surface lists no knowledge tools.
64
+ * Presence of the port makes declared knowledge tools available.
65
65
  */
66
66
  export interface KnowledgeSearchPort {
67
- enabled(): Promise<boolean>;
68
67
  search(componentName: string, request: {
69
68
  readonly query: string;
70
69
  readonly limit?: number | undefined;
@@ -73,7 +72,7 @@ export interface KnowledgeSearchPort {
73
72
  readonly hits: readonly KnowledgeSearchHit[];
74
73
  } | {
75
74
  readonly ok: false;
76
- readonly reason: 'budget_exhausted' | 'not_enabled' | 'provider_error';
75
+ readonly reason: 'budget_exhausted' | 'provider_error';
77
76
  readonly message: string;
78
77
  }>;
79
78
  }
@@ -1,3 +1,4 @@
1
+ import { ConversationCapture } from './conversation-history/capture.js';
1
2
  /** Browser and messaging resolve model, execution and tenant policy from one composition. */
2
3
  export function assistantRouteDependencies(options, core) {
3
4
  return {
@@ -13,6 +14,14 @@ export function assistantRouteDependencies(options, core) {
13
14
  : {}),
14
15
  ...(options.captureRequestEvent ? { captureRequestEvent: options.captureRequestEvent } : {}),
15
16
  ...(options.clock ? { clock: options.clock } : {}),
17
+ ...(options.conversationHistory
18
+ ? {
19
+ conversations: new ConversationCapture(options.conversationHistory.store, options.conversationHistory.policy, {
20
+ now: () => (options.clock?.() ?? new Date()).getTime(),
21
+ ...(core.logger ? { logger: core.logger } : {}),
22
+ }),
23
+ }
24
+ : {}),
16
25
  };
17
26
  }
18
27
  //# sourceMappingURL=assistant-composition.js.map