@qvac/harness 0.2.9 → 0.2.10

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 (42) hide show
  1. package/README.md +15 -7
  2. package/dist/harness.bundle +7 -59
  3. package/dist/prebuilds-manifest.json +0 -8
  4. package/dist-lib/lib/capabilities/completion.d.ts +1 -0
  5. package/dist-lib/lib/capabilities/completion.js +66 -9
  6. package/dist-lib/lib/connect.js +2 -1
  7. package/dist-lib/lib/contract-types.d.ts +1 -0
  8. package/dist-lib/lib/docx/index.js +4 -27
  9. package/dist-lib/lib/hooks/built-in/index.d.ts +1 -0
  10. package/dist-lib/lib/hooks/built-in/index.js +1 -0
  11. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +5 -0
  12. package/dist-lib/lib/hooks/built-in/sandbox.js +43 -0
  13. package/dist-lib/lib/hooks/built-in/skill-catalog.js +7 -2
  14. package/dist-lib/lib/hooks/registry.js +2 -1
  15. package/dist-lib/lib/hooks/types.d.ts +1 -0
  16. package/dist-lib/lib/office/index.d.ts +6 -0
  17. package/dist-lib/lib/office/index.js +38 -0
  18. package/dist-lib/lib/office/pptx.d.ts +1 -0
  19. package/dist-lib/lib/office/pptx.js +26 -0
  20. package/dist-lib/lib/office/python.d.ts +7 -0
  21. package/dist-lib/lib/office/python.js +32 -0
  22. package/dist-lib/lib/office/xlsx.d.ts +1 -0
  23. package/dist-lib/lib/office/xlsx.js +26 -0
  24. package/dist-lib/lib/tools/edit-image.js +1 -1
  25. package/dist-lib/lib/tools/generate-image.js +1 -1
  26. package/dist-lib/lib/tools/generate-music.js +1 -1
  27. package/dist-lib/lib/tools/http-request.js +3 -3
  28. package/dist-lib/lib/tools/mcp-call.js +3 -3
  29. package/dist-lib/lib/tools/skill.js +4 -2
  30. package/dist-lib/lib/tools/skills/auth-resolver.d.ts +1 -0
  31. package/dist-lib/lib/tools/skills/auth-resolver.js +59 -29
  32. package/dist-lib/lib/tools/toolsets.d.ts +1 -0
  33. package/dist-lib/lib/tools/toolsets.js +1 -0
  34. package/dist-lib/lib/tools/weather-lookup.js +9 -3
  35. package/dist-lib/lib/tools/web-fetch.js +6 -3
  36. package/dist-lib/lib/tools/web-search.js +5 -2
  37. package/dist-lib/schema/harness.js +2 -1
  38. package/dist-lib/spec/hrpc/messages.js +8 -5
  39. package/dist-lib/spec/hyperschema/index.js +8 -5
  40. package/dist-lib/spec/hyperschema/schema.json +6 -1
  41. package/dist-lib/spec/hyperschema/types.d.ts +2 -1
  42. package/package.json +5 -1
@@ -60,49 +60,79 @@ export function parseAuth(v) {
60
60
  }
61
61
  return { ok: true, value: { type: 'bearer', tokenCredentialKey: key } };
62
62
  }
63
- export async function resolveAuth(auth, url, credentialStore, credentialAllowList, ctx) {
64
- const key = auth.tokenCredentialKey;
65
- if (!credentialStore)
66
- return { error: 'credentials not configured' };
67
- const entry = credentialAllowList?.[key];
68
- if (!entry) {
63
+ function grantedKeys(url, credentialAllowList) {
64
+ return Object.entries(credentialAllowList)
65
+ .filter(([, entry]) => urlAllowed(url, Array.isArray(entry) ? entry : [entry]))
66
+ .map(([key]) => key);
67
+ }
68
+ async function connectedCredentials(keys, credentialStore, ctx) {
69
+ const connected = [];
70
+ for (const key of keys) {
71
+ const record = credentialRecord(await credentialStore(key, ctx));
72
+ if (record)
73
+ connected.push({ token: record.token, key });
74
+ }
75
+ return connected;
76
+ }
77
+ function ambiguous(keys) {
78
+ return {
79
+ error: `multiple credentials match this URL; set auth.tokenCredentialKey to one of ${keys.join(', ')}`
80
+ };
81
+ }
82
+ async function resolveGuessedKey(key, url, credentialStore, credentialAllowList, ctx) {
83
+ const granted = grantedKeys(url, credentialAllowList);
84
+ if (granted.length === 0) {
69
85
  return {
70
- error: `credential ${key} is not available here — load the skill that declares it first`
86
+ error: `unknown credential key ${key}; no loaded skill grants a credential for ${url.host} — omit auth, or load the skill that declares it`
71
87
  };
72
88
  }
89
+ const [first, ...rest] = await connectedCredentials(granted, credentialStore, ctx);
90
+ if (!first) {
91
+ return {
92
+ error: `unknown credential key ${key}; no connected credential for ${url.host} (${granted.join(', ')}). Tell the user to connect it in Skills; do not retry`
93
+ };
94
+ }
95
+ if (rest.length > 0)
96
+ return ambiguous([first, ...rest].map((c) => c.key));
97
+ return first;
98
+ }
99
+ async function resolveDeclaredKey(key, url, credentialStore, credentialAllowList, ctx) {
100
+ const entry = credentialAllowList?.[key];
101
+ if (!entry)
102
+ return null;
73
103
  const patterns = Array.isArray(entry) ? entry : [entry];
74
104
  if (!urlAllowed(url, patterns)) {
75
105
  return { error: `credential ${key} is not allowed for URL ${url.href}` };
76
106
  }
77
107
  const record = credentialRecord(await credentialStore(key, ctx));
78
- if (!record)
79
- return { error: `unknown credential: ${key}` };
80
- return { token: record.token };
108
+ if (!record) {
109
+ return {
110
+ error: `credential ${key} is not connected for ${url.host}. Tell the user to connect it in Skills; do not retry`
111
+ };
112
+ }
113
+ return { token: record.token, key };
114
+ }
115
+ export async function resolveAuth(auth, url, credentialStore, credentialAllowList, ctx) {
116
+ const key = auth.tokenCredentialKey;
117
+ if (!credentialStore)
118
+ return { error: 'credentials not configured' };
119
+ return ((await resolveDeclaredKey(key, url, credentialStore, credentialAllowList, ctx)) ??
120
+ resolveGuessedKey(key, url, credentialStore, credentialAllowList ?? {}, ctx));
81
121
  }
82
122
  export async function resolveAutoAuth(url, credentialStore, credentialAllowList, ctx) {
83
123
  if (!credentialStore || !credentialAllowList)
84
124
  return null;
85
- let selected = null;
86
- for (const [key, entry] of Object.entries(credentialAllowList)) {
87
- const patterns = Array.isArray(entry) ? entry : [entry];
88
- if (!urlAllowed(url, patterns))
89
- continue;
90
- const record = credentialRecord(await credentialStore(key, ctx));
91
- if (!record)
92
- continue;
93
- if (selected) {
94
- return { error: 'multiple credentials match this URL; set auth.tokenCredentialKey' };
95
- }
96
- selected = { token: record.token, key };
97
- }
98
- return selected;
125
+ const connected = await connectedCredentials(grantedKeys(url, credentialAllowList), credentialStore, ctx);
126
+ if (connected.length > 1)
127
+ return ambiguous(connected.map((c) => c.key));
128
+ return connected[0] ?? null;
99
129
  }
100
130
  export async function resolveBearerHeader(auth, url, credentialStore, credentialAllowList, ctx, signal) {
101
131
  if (auth) {
102
132
  const resolved = await raceAbort(resolveAuth(auth, url, credentialStore, credentialAllowList, ctx), signal);
103
133
  if ('error' in resolved)
104
134
  return { error: resolved.error };
105
- return { header: `Bearer ${resolved.token}`, credentialKey: auth.tokenCredentialKey };
135
+ return { header: `Bearer ${resolved.token}`, credentialKey: resolved.key };
106
136
  }
107
137
  const resolved = await raceAbort(resolveAutoAuth(url, credentialStore, credentialAllowList, ctx), signal);
108
138
  if (resolved && 'error' in resolved)
@@ -112,10 +142,10 @@ export async function resolveBearerHeader(auth, url, credentialStore, credential
112
142
  : { header: null };
113
143
  }
114
144
  export async function resolveRedirectBearerHeader(auth, url, credentialStore, credentialAllowList, ctx, signal) {
115
- if (!auth)
145
+ if (!auth || !credentialStore)
116
146
  return { header: null };
117
- const resolved = await raceAbort(resolveAuth(auth, url, credentialStore, credentialAllowList, ctx), signal);
118
- return 'error' in resolved
147
+ const resolved = await raceAbort(resolveDeclaredKey(auth.tokenCredentialKey, url, credentialStore, credentialAllowList, ctx), signal);
148
+ return !resolved || 'error' in resolved
119
149
  ? { header: null }
120
- : { header: `Bearer ${resolved.token}`, credentialKey: auth.tokenCredentialKey };
150
+ : { header: `Bearer ${resolved.token}`, credentialKey: resolved.key };
121
151
  }
@@ -4,5 +4,6 @@ export declare function asToolset(value?: string | null): Toolset | null;
4
4
  export interface ToolsetScoped {
5
5
  toolset?: string | null;
6
6
  }
7
+ export declare const NETWORK_TOOLSETS: ReadonlyArray<Toolset>;
7
8
  export declare function permittedTools<T extends ToolsetScoped>(tools: ReadonlyArray<T>, disabled?: ReadonlyArray<string> | null): T[];
8
9
  export declare function toolsetEnabled(toolset: Toolset, disabled?: ReadonlyArray<string> | null): boolean;
@@ -9,6 +9,7 @@ export const TOOLSETS = [
9
9
  export function asToolset(value) {
10
10
  return TOOLSETS.find((toolset) => toolset === value) ?? null;
11
11
  }
12
+ export const NETWORK_TOOLSETS = ['web-search', 'http-request', 'mcp-call'];
12
13
  export function permittedTools(tools, disabled) {
13
14
  if (!disabled?.length)
14
15
  return [...tools];
@@ -63,7 +63,7 @@ export function createWeatherLookupTool({ fetch: fetchImpl, resolveHost = lookup
63
63
  if (/location not found/i.test(body))
64
64
  return { content: notFound(location) };
65
65
  if (response.status !== 200) {
66
- return { error: unavailable(`it answered HTTP ${response.status}`) };
66
+ return { error: unavailable(`it answered HTTP ${response.status}`), code: 'transport' };
67
67
  }
68
68
  let json;
69
69
  try {
@@ -91,9 +91,15 @@ export function createWeatherLookupTool({ fetch: fetchImpl, resolveHost = lookup
91
91
  if (ctx?.signal?.aborted)
92
92
  return { error: 'weather_lookup cancelled' };
93
93
  if (controller.signal.aborted) {
94
- return { error: `weather_lookup timed out after ${LOOKUP_TIMEOUT_MS} ms` };
94
+ return {
95
+ error: `weather_lookup timed out after ${LOOKUP_TIMEOUT_MS} ms`,
96
+ code: 'timeout'
97
+ };
95
98
  }
96
- return { error: unavailable(err instanceof Error ? err.message : String(err)) };
99
+ return {
100
+ error: unavailable(err instanceof Error ? err.message : String(err)),
101
+ code: 'transport'
102
+ };
97
103
  }
98
104
  finally {
99
105
  clearTimeout(timeout);
@@ -79,7 +79,10 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
79
79
  return { error: got.error };
80
80
  const { response, finalUrl: currentUrl } = got;
81
81
  if (response.status < 200 || response.status >= 300) {
82
- return { error: `HTTP ${response.status} fetching ${currentUrl.toString()}` };
82
+ return {
83
+ error: `HTTP ${response.status} fetching ${currentUrl.toString()}`,
84
+ status: response.status
85
+ };
83
86
  }
84
87
  const rawBody = await response.text();
85
88
  const body = rawBody.length > MAX_HTML_CHARS ? rawBody.slice(0, MAX_HTML_CHARS) : rawBody;
@@ -140,9 +143,9 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
140
143
  return { error: 'web_fetch cancelled' };
141
144
  }
142
145
  if (controller.signal.aborted) {
143
- return { error: `${url.hostname} timed out after ${PAGE_TIMEOUT_MS}ms` };
146
+ return { error: `${url.hostname} timed out after ${PAGE_TIMEOUT_MS}ms`, code: 'timeout' };
144
147
  }
145
- return { error: err instanceof Error ? err.message : String(err) };
148
+ return { error: err instanceof Error ? err.message : String(err), code: 'transport' };
146
149
  }
147
150
  finally {
148
151
  clearTimeout(timeout);
@@ -43,12 +43,15 @@ export function createWebSearchTool({ fetch } = {}) {
43
43
  }
44
44
  const failed = engineSummary.filter((e) => e.status !== 'ok').length;
45
45
  if (engineSummary.length > 0 && failed === engineSummary.length) {
46
- return { error: `web_search unavailable: all ${failed} engines failed (timeout/error)` };
46
+ return {
47
+ error: `web_search unavailable: all ${failed} engines failed (timeout/error)`,
48
+ code: 'transport'
49
+ };
47
50
  }
48
51
  return { content: `No web results found for "${query}".` };
49
52
  }
50
53
  catch (err) {
51
- return { error: err instanceof Error ? err.message : String(err) };
54
+ return { error: err instanceof Error ? err.message : String(err), code: 'transport' };
52
55
  }
53
56
  }
54
57
  };
@@ -23,7 +23,8 @@ export function registerHarnessTypes(ns) {
23
23
  { name: 'imageWidth', type: 'uint' },
24
24
  { name: 'imageHeight', type: 'uint' },
25
25
  { name: 'approvalMode', type: '@harness/approval-mode' },
26
- { name: 'disabledToolsets', type: 'string', array: true }
26
+ { name: 'disabledToolsets', type: 'string', array: true },
27
+ { name: 'sandboxMode', type: 'bool' }
27
28
  ]
28
29
  });
29
30
  ns.register({
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 9
2
+ // Schema Version: 10
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
5
  /* eslint-disable space-before-function-paren */
6
6
 
7
7
  import { c } from 'hyperschema/runtime'
8
8
 
9
- const VERSION = 9
9
+ const VERSION = 10
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -143,7 +143,8 @@ const encoding4 = {
143
143
  (m.imageWidth ? 8192 : 0) |
144
144
  (m.imageHeight ? 16384 : 0) |
145
145
  ((version >= 5 && m.approvalMode) ? 32768 : 0) |
146
- ((version >= 5 && m.disabledToolsets) ? 65536 : 0)
146
+ ((version >= 5 && m.disabledToolsets) ? 65536 : 0) |
147
+ ((version >= 10 && m.sandboxMode) ? 131072 : 0)
147
148
 
148
149
  c.uint.preencode(state, flags)
149
150
 
@@ -183,7 +184,8 @@ const encoding4 = {
183
184
  (m.imageWidth ? 8192 : 0) |
184
185
  (m.imageHeight ? 16384 : 0) |
185
186
  ((version >= 5 && m.approvalMode) ? 32768 : 0) |
186
- ((version >= 5 && m.disabledToolsets) ? 65536 : 0)
187
+ ((version >= 5 && m.disabledToolsets) ? 65536 : 0) |
188
+ ((version >= 10 && m.sandboxMode) ? 131072 : 0)
187
189
 
188
190
  c.uint.encode(state, flags)
189
191
 
@@ -225,7 +227,8 @@ const encoding4 = {
225
227
  imageWidth: (flags & 8192) !== 0 ? c.uint.decode(state) : 0,
226
228
  imageHeight: (flags & 16384) !== 0 ? c.uint.decode(state) : 0,
227
229
  approvalMode: (version >= 5 && (flags & 32768) !== 0) ? encoding59.decode(state) : null,
228
- disabledToolsets: (version >= 5 && (flags & 65536) !== 0) ? encoding4_16.decode(state) : null
230
+ disabledToolsets: (version >= 5 && (flags & 65536) !== 0) ? encoding4_16.decode(state) : null,
231
+ sandboxMode: (version >= 10 && (flags & 131072) !== 0)
229
232
  }
230
233
  }
231
234
  }
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 9
2
+ // Schema Version: 10
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
5
  /* eslint-disable space-before-function-paren */
6
6
 
7
7
  import { c } from 'hyperschema/runtime'
8
8
 
9
- const VERSION = 9
9
+ const VERSION = 10
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -143,7 +143,8 @@ const encoding4 = {
143
143
  (m.imageWidth ? 8192 : 0) |
144
144
  (m.imageHeight ? 16384 : 0) |
145
145
  ((version >= 5 && m.approvalMode) ? 32768 : 0) |
146
- ((version >= 5 && m.disabledToolsets) ? 65536 : 0)
146
+ ((version >= 5 && m.disabledToolsets) ? 65536 : 0) |
147
+ ((version >= 10 && m.sandboxMode) ? 131072 : 0)
147
148
 
148
149
  c.uint.preencode(state, flags)
149
150
 
@@ -183,7 +184,8 @@ const encoding4 = {
183
184
  (m.imageWidth ? 8192 : 0) |
184
185
  (m.imageHeight ? 16384 : 0) |
185
186
  ((version >= 5 && m.approvalMode) ? 32768 : 0) |
186
- ((version >= 5 && m.disabledToolsets) ? 65536 : 0)
187
+ ((version >= 5 && m.disabledToolsets) ? 65536 : 0) |
188
+ ((version >= 10 && m.sandboxMode) ? 131072 : 0)
187
189
 
188
190
  c.uint.encode(state, flags)
189
191
 
@@ -225,7 +227,8 @@ const encoding4 = {
225
227
  imageWidth: (flags & 8192) !== 0 ? c.uint.decode(state) : 0,
226
228
  imageHeight: (flags & 16384) !== 0 ? c.uint.decode(state) : 0,
227
229
  approvalMode: (version >= 5 && (flags & 32768) !== 0) ? encoding59.decode(state) : null,
228
- disabledToolsets: (version >= 5 && (flags & 65536) !== 0) ? encoding4_16.decode(state) : null
230
+ disabledToolsets: (version >= 5 && (flags & 65536) !== 0) ? encoding4_16.decode(state) : null,
231
+ sandboxMode: (version >= 10 && (flags & 131072) !== 0)
229
232
  }
230
233
  }
231
234
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 9,
2
+ "version": 10,
3
3
  "schema": [
4
4
  {
5
5
  "name": "empty",
@@ -151,6 +151,11 @@
151
151
  "array": true,
152
152
  "type": "string",
153
153
  "version": 5
154
+ },
155
+ {
156
+ "name": "sandboxMode",
157
+ "type": "bool",
158
+ "version": 10
154
159
  }
155
160
  ]
156
161
  },
@@ -1,5 +1,5 @@
1
1
  // This file is autogenerated by hyperschema-ts
2
- // Schema version: 9
2
+ // Schema version: 10
3
3
  // Do not edit manually.
4
4
 
5
5
  export interface HarnessEmpty {
@@ -31,6 +31,7 @@ export interface HarnessAgentKnobs {
31
31
  imageHeight?: number
32
32
  approvalMode?: HarnessApprovalMode | null
33
33
  disabledToolsets?: string[] | null
34
+ sandboxMode?: boolean
34
35
  }
35
36
 
36
37
  export type HarnessMessageRole = "system" | "user" | "assistant" | "tool"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/harness",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "description": "An AI harness implementing generate(context) via @qvac/sdk. Knows nothing about workbench, hyperdb, or chunks — just inference in, events out.",
6
6
  "exports": {
@@ -12,6 +12,10 @@
12
12
  "types": "./dist-lib/lib/docx/index.d.ts",
13
13
  "default": "./dist-lib/lib/docx/index.js"
14
14
  },
15
+ "./office": {
16
+ "types": "./dist-lib/lib/office/index.d.ts",
17
+ "default": "./dist-lib/lib/office/index.js"
18
+ },
15
19
  "./lib/dns-lookup.ts": {
16
20
  "types": "./dist-lib/lib/dns-lookup.d.ts",
17
21
  "default": "./dist-lib/lib/dns-lookup.js"