@qvac/harness 0.2.9 → 0.2.12

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 (57) hide show
  1. package/README.md +15 -7
  2. package/dist/harness.bundle +11 -62
  3. package/dist/prebuilds-manifest.json +0 -8
  4. package/dist-lib/index.d.ts +16 -1
  5. package/dist-lib/index.js +31 -1
  6. package/dist-lib/lib/capabilities/completion.d.ts +1 -0
  7. package/dist-lib/lib/capabilities/completion.js +66 -9
  8. package/dist-lib/lib/capabilities/detect-language.d.ts +11 -0
  9. package/dist-lib/lib/capabilities/detect-language.js +13 -0
  10. package/dist-lib/lib/capabilities/invoke.d.ts +15 -0
  11. package/dist-lib/lib/capabilities/invoke.js +33 -0
  12. package/dist-lib/lib/connect.d.ts +4 -1
  13. package/dist-lib/lib/connect.js +51 -22
  14. package/dist-lib/lib/contract-types.d.ts +21 -0
  15. package/dist-lib/lib/docx/index.js +4 -27
  16. package/dist-lib/lib/hooks/built-in/index.d.ts +1 -0
  17. package/dist-lib/lib/hooks/built-in/index.js +1 -0
  18. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +5 -0
  19. package/dist-lib/lib/hooks/built-in/sandbox.js +45 -0
  20. package/dist-lib/lib/hooks/built-in/skill-catalog.js +7 -2
  21. package/dist-lib/lib/hooks/registry.js +2 -1
  22. package/dist-lib/lib/hooks/types.d.ts +1 -0
  23. package/dist-lib/lib/office/index.d.ts +6 -0
  24. package/dist-lib/lib/office/index.js +38 -0
  25. package/dist-lib/lib/office/pptx.d.ts +1 -0
  26. package/dist-lib/lib/office/pptx.js +26 -0
  27. package/dist-lib/lib/office/python.d.ts +7 -0
  28. package/dist-lib/lib/office/python.js +32 -0
  29. package/dist-lib/lib/office/xlsx.d.ts +1 -0
  30. package/dist-lib/lib/office/xlsx.js +26 -0
  31. package/dist-lib/lib/serve.js +32 -4
  32. package/dist-lib/lib/tools/edit-image.js +1 -1
  33. package/dist-lib/lib/tools/generate-image.js +1 -1
  34. package/dist-lib/lib/tools/generate-music.js +1 -1
  35. package/dist-lib/lib/tools/http-request.js +4 -4
  36. package/dist-lib/lib/tools/mcp-call.js +3 -3
  37. package/dist-lib/lib/tools/skill.js +4 -2
  38. package/dist-lib/lib/tools/skills/auth-resolver.d.ts +1 -0
  39. package/dist-lib/lib/tools/skills/auth-resolver.js +59 -29
  40. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  41. package/dist-lib/lib/tools/skills/manifest-parse.js +92 -1
  42. package/dist-lib/lib/tools/skills/manifest.d.ts +12 -0
  43. package/dist-lib/lib/tools/toolsets.d.ts +1 -0
  44. package/dist-lib/lib/tools/toolsets.js +1 -0
  45. package/dist-lib/lib/tools/weather-lookup.js +9 -3
  46. package/dist-lib/lib/tools/web-fetch.js +18 -5
  47. package/dist-lib/lib/tools/web-search.js +5 -2
  48. package/dist-lib/schema/harness.js +54 -4
  49. package/dist-lib/spec/bind.js +3 -0
  50. package/dist-lib/spec/calls.js +1 -0
  51. package/dist-lib/spec/hrpc/hrpc.json +13 -0
  52. package/dist-lib/spec/hrpc/index.js +15 -3
  53. package/dist-lib/spec/hrpc/messages.js +207 -13
  54. package/dist-lib/spec/hyperschema/index.js +207 -13
  55. package/dist-lib/spec/hyperschema/schema.json +163 -1
  56. package/dist-lib/spec/hyperschema/types.d.ts +43 -2
  57. package/package.json +7 -2
@@ -29,6 +29,17 @@ export interface SkillSetupField {
29
29
  readonly secret?: boolean;
30
30
  readonly required?: boolean;
31
31
  }
32
+ export interface SkillOAuthConfig {
33
+ readonly stateKey: string;
34
+ readonly authUrl: string;
35
+ readonly tokenUrl: string;
36
+ readonly scopes: readonly string[];
37
+ readonly tokenAuth: 'pkce-only' | 'secret-in-body' | 'secret-in-basic';
38
+ readonly extraAuthParams?: Readonly<Record<string, string>>;
39
+ readonly registrationEndpoint?: string;
40
+ readonly port?: number;
41
+ readonly sharedIosSession?: boolean;
42
+ }
32
43
  export interface SkillSetupRoute {
33
44
  readonly kind: 'oauth' | 'token' | 'install' | 'picker' | 'instructions';
34
45
  readonly label: string;
@@ -40,6 +51,7 @@ export interface SkillSetupRoute {
40
51
  readonly configSteps?: readonly string[];
41
52
  readonly fields?: readonly SkillSetupField[];
42
53
  readonly satisfied?: boolean;
54
+ readonly oauth?: SkillOAuthConfig;
43
55
  }
44
56
  export interface SkillSetup {
45
57
  readonly summary?: string;
@@ -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);
@@ -28,7 +28,11 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
28
28
  toolset: 'web-search',
29
29
  description: 'Fetch a public web page and return its readable text. ' +
30
30
  'Use this to read the full content behind a web_search result URL, or a page the user ' +
31
- 'shares, when a search snippet is not enough to answer.',
31
+ 'shares, when a search snippet is not enough to answer. ' +
32
+ 'When the URL serves a file rather than a page — PDF, .docx, .xlsx, .pptx, an image, any ' +
33
+ 'binary — this is also the tool that downloads it: the bytes become a chat attachment and ' +
34
+ 'the result carries its attachmentId. It is the only way to get a remote file into the ' +
35
+ 'chat; exec has no network and http_request truncates binary bodies.',
32
36
  parameters: {
33
37
  type: 'object',
34
38
  properties: {
@@ -79,7 +83,10 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
79
83
  return { error: got.error };
80
84
  const { response, finalUrl: currentUrl } = got;
81
85
  if (response.status < 200 || response.status >= 300) {
82
- return { error: `HTTP ${response.status} fetching ${currentUrl.toString()}` };
86
+ return {
87
+ error: `HTTP ${response.status} fetching ${currentUrl.toString()}`,
88
+ status: response.status
89
+ };
83
90
  }
84
91
  const rawBody = await response.text();
85
92
  const body = rawBody.length > MAX_HTML_CHARS ? rawBody.slice(0, MAX_HTML_CHARS) : rawBody;
@@ -140,9 +147,9 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
140
147
  return { error: 'web_fetch cancelled' };
141
148
  }
142
149
  if (controller.signal.aborted) {
143
- return { error: `${url.hostname} timed out after ${PAGE_TIMEOUT_MS}ms` };
150
+ return { error: `${url.hostname} timed out after ${PAGE_TIMEOUT_MS}ms`, code: 'timeout' };
144
151
  }
145
- return { error: err instanceof Error ? err.message : String(err) };
152
+ return { error: err instanceof Error ? err.message : String(err), code: 'transport' };
146
153
  }
147
154
  finally {
148
155
  clearTimeout(timeout);
@@ -175,8 +182,14 @@ async function attachDocument(response, url, mimeType, attach) {
175
182
  raw: !mime.startsWith('image/')
176
183
  });
177
184
  return {
185
+ attachmentId: record.id,
186
+ fileName: record.fileName,
187
+ byteLength: record.byteLength,
178
188
  content: `Attached the file "${record.fileName}" (${Math.round(record.byteLength / 1024)}KB, ${mime}) from ${url.toString()} — ` +
179
- 'the user can open or save it. Its contents were NOT read: do not claim to know what is inside.'
189
+ 'the user can open or save it. Its contents were NOT read, so do not describe what is inside ' +
190
+ 'yet. To read it, stage it with exec as ' +
191
+ `inputs [{ "attachmentId": "${record.id}", "path": "${record.fileName}" }] — never ask the ` +
192
+ 'user to attach a file this call already attached.'
180
193
  };
181
194
  }
182
195
  const MIME_EXTENSIONS = {
@@ -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({
@@ -68,7 +69,8 @@ export function registerHarnessTypes(ns) {
68
69
  'fetch-result',
69
70
  'rag-result',
70
71
  'question-result',
71
- 'drive-result'
72
+ 'drive-result',
73
+ 'invoke'
72
74
  ],
73
75
  strings: true
74
76
  });
@@ -108,7 +110,9 @@ export function registerHarnessTypes(ns) {
108
110
  { name: 'grantedDrives', type: '@harness/granted-drive', array: true },
109
111
  { name: 'payload', type: 'json' },
110
112
  { name: 'grantedWriteDirs', type: 'string', array: true },
111
- { name: 'captureInferenceCalls', type: 'bool' }
113
+ { name: 'captureInferenceCalls', type: 'bool' },
114
+ { name: 'capability', type: 'string' },
115
+ { name: 'arguments', type: 'json' }
112
116
  ]
113
117
  });
114
118
  ns.register({
@@ -458,6 +462,25 @@ export function registerHarnessTypes(ns) {
458
462
  name: 'transcribe-response',
459
463
  fields: [{ name: 'text', type: 'string', required: true }]
460
464
  });
465
+ ns.register({
466
+ name: 'detect-language-request',
467
+ fields: [
468
+ { name: 'text', type: 'string', required: true },
469
+ { name: 'topK', type: 'uint' }
470
+ ]
471
+ });
472
+ ns.register({
473
+ name: 'detected-language',
474
+ fields: [
475
+ { name: 'code', type: 'string', required: true },
476
+ { name: 'language', type: 'string', required: true },
477
+ { name: 'probability', type: 'float64' }
478
+ ]
479
+ });
480
+ ns.register({
481
+ name: 'detect-language-response',
482
+ fields: [{ name: 'languages', type: '@harness/detected-language', array: true, required: true }]
483
+ });
461
484
  ns.register({
462
485
  name: 'speak-request',
463
486
  fields: [
@@ -506,7 +529,8 @@ export function registerHarnessTypes(ns) {
506
529
  { name: 'steps', type: 'string', array: true },
507
530
  { name: 'configSteps', type: 'string', array: true },
508
531
  { name: 'fields', type: '@harness/skill-setup-field', array: true },
509
- { name: 'satisfied', type: 'bool' }
532
+ { name: 'satisfied', type: 'bool' },
533
+ { name: 'oauth', type: '@harness/skill-oauth-config' }
510
534
  ]
511
535
  });
512
536
  ns.register({
@@ -613,6 +637,27 @@ export function registerHarnessTypes(ns) {
613
637
  name: 'kv-cache-delete-request',
614
638
  fields: [{ name: 'kvCacheKey', type: 'string', required: true }]
615
639
  });
640
+ ns.register({
641
+ name: 'skill-oauth-param',
642
+ fields: [
643
+ { name: 'key', type: 'string', required: true },
644
+ { name: 'value', type: 'string', required: true }
645
+ ]
646
+ });
647
+ ns.register({
648
+ name: 'skill-oauth-config',
649
+ fields: [
650
+ { name: 'stateKey', type: 'string', required: true },
651
+ { name: 'authUrl', type: 'string', required: true },
652
+ { name: 'tokenUrl', type: 'string', required: true },
653
+ { name: 'tokenAuth', type: 'string', required: true },
654
+ { name: 'scopes', type: 'string', array: true },
655
+ { name: 'extraAuthParams', type: '@harness/skill-oauth-param', array: true },
656
+ { name: 'registrationEndpoint', type: 'string' },
657
+ { name: 'port', type: 'uint' },
658
+ { name: 'sharedIosSession', type: 'bool' }
659
+ ]
660
+ });
616
661
  }
617
662
  export function registerHarnessApi(api) {
618
663
  api.register({
@@ -755,4 +800,9 @@ export function registerHarnessApi(api) {
755
800
  request: { name: '@harness/kv-cache-delete-request', stream: false },
756
801
  response: { name: '@harness/kv-cache-deletion-response', stream: false }
757
802
  });
803
+ api.register({
804
+ name: 'detect-language',
805
+ request: { name: '@harness/detect-language-request', stream: false },
806
+ response: { name: '@harness/detect-language-response', stream: false }
807
+ });
758
808
  }
@@ -124,4 +124,7 @@ export function bindApi(rpc, api) {
124
124
  if (api.deleteKvCache) {
125
125
  rpc.onDeleteKvCache((req) => api.deleteKvCache(req));
126
126
  }
127
+ if (api.detectLanguage) {
128
+ rpc.onDetectLanguage((req) => api.detectLanguage(req));
129
+ }
127
130
  }
@@ -38,5 +38,6 @@ export function harnessClient(rpc) {
38
38
  copySkillForEdit: (input) => rpc.copySkillForEdit(input),
39
39
  deleteOverlaySkill: (input) => rpc.deleteOverlaySkill(input),
40
40
  deleteKvCache: (input) => rpc.deleteKvCache(input),
41
+ detectLanguage: (input) => rpc.detectLanguage(input),
41
42
  };
42
43
  }
@@ -364,6 +364,19 @@
364
364
  "stream": false
365
365
  },
366
366
  "version": 1
367
+ },
368
+ {
369
+ "id": 28,
370
+ "name": "@harness/detect-language",
371
+ "request": {
372
+ "name": "@harness/detect-language-request",
373
+ "stream": false
374
+ },
375
+ "response": {
376
+ "name": "@harness/detect-language-response",
377
+ "stream": false
378
+ },
379
+ "version": 1
367
380
  }
368
381
  ]
369
382
  }
@@ -61,7 +61,9 @@ const methods = new Map([
61
61
  ['@harness/delete-overlay-skill', 26],
62
62
  [26, '@harness/delete-overlay-skill'],
63
63
  ['@harness/delete-kv-cache', 27],
64
- [27, '@harness/delete-kv-cache']
64
+ [27, '@harness/delete-kv-cache'],
65
+ ['@harness/detect-language', 28],
66
+ [28, '@harness/detect-language']
65
67
  ])
66
68
 
67
69
  class HRPC {
@@ -96,7 +98,8 @@ class HRPC {
96
98
  ['@harness/write-skill-file', getEncoding('@harness/write-skill-request')],
97
99
  ['@harness/copy-skill-for-edit', getEncoding('@harness/skill-name-request')],
98
100
  ['@harness/delete-overlay-skill', getEncoding('@harness/skill-name-request')],
99
- ['@harness/delete-kv-cache', getEncoding('@harness/kv-cache-delete-request')]
101
+ ['@harness/delete-kv-cache', getEncoding('@harness/kv-cache-delete-request')],
102
+ ['@harness/detect-language', getEncoding('@harness/detect-language-request')]
100
103
  ])
101
104
  this._responseEncodings = new Map([
102
105
  ['@harness/completion', getEncoding('@harness/completion-event')],
@@ -126,7 +129,8 @@ class HRPC {
126
129
  ['@harness/write-skill-file', getEncoding('@harness/ok-response')],
127
130
  ['@harness/copy-skill-for-edit', getEncoding('@harness/copy-skill-response')],
128
131
  ['@harness/delete-overlay-skill', getEncoding('@harness/delete-skill-response')],
129
- ['@harness/delete-kv-cache', getEncoding('@harness/kv-cache-deletion-response')]
132
+ ['@harness/delete-kv-cache', getEncoding('@harness/kv-cache-deletion-response')],
133
+ ['@harness/detect-language', getEncoding('@harness/detect-language-response')]
130
134
  ])
131
135
  this._rpc = new RPC(stream, async (req) => {
132
136
  const command = methods.get(req.command)
@@ -336,6 +340,10 @@ class HRPC {
336
340
  return this._call('@harness/delete-kv-cache', args)
337
341
  }
338
342
 
343
+ async detectLanguage(args) {
344
+ return this._call('@harness/detect-language', args)
345
+ }
346
+
339
347
  onCompletion(responseFn) {
340
348
  this._handlers['@harness/completion'] = responseFn
341
349
  }
@@ -448,6 +456,10 @@ class HRPC {
448
456
  this._handlers['@harness/delete-kv-cache'] = responseFn
449
457
  }
450
458
 
459
+ onDetectLanguage(responseFn) {
460
+ this._handlers['@harness/detect-language'] = responseFn
461
+ }
462
+
451
463
  _requestIsStream(command) {
452
464
  return [
453
465
  '@harness/completion',