@husk-ai/core 0.1.1

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 (61) hide show
  1. package/dist/audit.d.ts +65 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +87 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/browse.d.ts +66 -0
  6. package/dist/browse.d.ts.map +1 -0
  7. package/dist/browse.js +217 -0
  8. package/dist/browse.js.map +1 -0
  9. package/dist/config.d.ts +73 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +61 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/errors.d.ts +59 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +40 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/ids.d.ts +7 -0
  18. package/dist/ids.d.ts.map +1 -0
  19. package/dist/ids.js +29 -0
  20. package/dist/ids.js.map +1 -0
  21. package/dist/index.d.ts +23 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +19 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lifecycle.d.ts +38 -0
  26. package/dist/lifecycle.d.ts.map +1 -0
  27. package/dist/lifecycle.js +52 -0
  28. package/dist/lifecycle.js.map +1 -0
  29. package/dist/logger.d.ts +20 -0
  30. package/dist/logger.d.ts.map +1 -0
  31. package/dist/logger.js +59 -0
  32. package/dist/logger.js.map +1 -0
  33. package/dist/net.d.ts +48 -0
  34. package/dist/net.d.ts.map +1 -0
  35. package/dist/net.js +219 -0
  36. package/dist/net.js.map +1 -0
  37. package/dist/spec.d.ts +832 -0
  38. package/dist/spec.d.ts.map +1 -0
  39. package/dist/spec.js +191 -0
  40. package/dist/spec.js.map +1 -0
  41. package/dist/types/agent.d.ts +168 -0
  42. package/dist/types/agent.d.ts.map +1 -0
  43. package/dist/types/agent.js +2 -0
  44. package/dist/types/agent.js.map +1 -0
  45. package/dist/types/computer.d.ts +205 -0
  46. package/dist/types/computer.d.ts.map +1 -0
  47. package/dist/types/computer.js +9 -0
  48. package/dist/types/computer.js.map +1 -0
  49. package/dist/types/model.d.ts +197 -0
  50. package/dist/types/model.d.ts.map +1 -0
  51. package/dist/types/model.js +16 -0
  52. package/dist/types/model.js.map +1 -0
  53. package/dist/types/transcript.d.ts +64 -0
  54. package/dist/types/transcript.d.ts.map +1 -0
  55. package/dist/types/transcript.js +3 -0
  56. package/dist/types/transcript.js.map +1 -0
  57. package/dist/util.d.ts +69 -0
  58. package/dist/util.d.ts.map +1 -0
  59. package/dist/util.js +236 -0
  60. package/dist/util.js.map +1 -0
  61. package/package.json +26 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * What the computer was asked to do, and what happened.
3
+ *
4
+ * Agent runs have always persisted their events. The MCP path -- Claude Code or
5
+ * Cursor calling `shell`, `write_file`, `browser_click` against a real machine
6
+ * -- recorded nothing durable at all: one `log.info` to stderr at startup and
7
+ * silence thereafter. That is the integration husk exists for, and it was the
8
+ * one with no answer to "what did it actually do in there".
9
+ *
10
+ * The shape is deliberately small, because an audit log nobody can read is the
11
+ * same as no audit log:
12
+ *
13
+ * - one NDJSON line per tool call, appended, never rewritten
14
+ * - one file per computer, so a machine's whole history is `cat`-able
15
+ * - arguments summarised rather than stored whole; a 200 KB `write_file`
16
+ * body is not evidence, its path and size are
17
+ * - secrets passed through the same redactor as every other output, because
18
+ * the log is exactly where a leaked token would sit undisturbed for months
19
+ *
20
+ * Fire-and-forget by design: auditing must never fail the operation it is
21
+ * describing, and a full disk should cost you the record, not the work.
22
+ */
23
+ export interface AuditEntry {
24
+ /** ISO 8601, when the call returned. */
25
+ at: string;
26
+ computerId: string;
27
+ /** Where the call came in from: `mcp`, `api`, `cli`. */
28
+ via: string;
29
+ tool: string;
30
+ /** Short, redacted summary of the arguments -- never the full payload. */
31
+ args: string;
32
+ ok: boolean;
33
+ durationMs: number;
34
+ /** First line of the failure, when there was one. */
35
+ error?: string;
36
+ }
37
+ /**
38
+ * A one-line description of a call's arguments.
39
+ *
40
+ * Values are truncated hard and the whole thing is redacted, so a token passed
41
+ * as an argument does not get a permanent home in the log.
42
+ */
43
+ export declare function summariseArgs(args: Record<string, unknown>): string;
44
+ /** `~/.husk/audit/<computerId>.ndjson` -- one file per machine. */
45
+ export declare function auditPath(computerId: string): string;
46
+ /**
47
+ * Append one entry. Never throws.
48
+ *
49
+ * The caller is on the response path of a tool the model is waiting for, so
50
+ * this does not block it and cannot break it.
51
+ */
52
+ export declare function recordAudit(entry: AuditEntry): Promise<void>;
53
+ /**
54
+ * Time a call and record it, whatever the outcome.
55
+ *
56
+ * Wrapping rather than two call sites so a future tool cannot be added and
57
+ * quietly skip the log -- which is how the MCP path came to have none.
58
+ */
59
+ export declare function audited<T>(meta: {
60
+ computerId: string;
61
+ via: string;
62
+ tool: string;
63
+ args: Record<string, unknown>;
64
+ }, run: () => Promise<T>, failed?: (value: T) => string | undefined): Promise<T>;
65
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiBnE;AAED,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlE;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAC7B,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EACtF,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACrB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,GACxC,OAAO,CAAC,CAAC,CAAC,CA0BZ"}
package/dist/audit.js ADDED
@@ -0,0 +1,87 @@
1
+ import { appendFile, mkdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { paths } from './config.js';
4
+ import { redact } from './util.js';
5
+ /** Keep a summary readable and bounded; the full payload is not the point. */
6
+ const MAX_ARG_CHARS = 300;
7
+ /**
8
+ * A one-line description of a call's arguments.
9
+ *
10
+ * Values are truncated hard and the whole thing is redacted, so a token passed
11
+ * as an argument does not get a permanent home in the log.
12
+ */
13
+ export function summariseArgs(args) {
14
+ const parts = [];
15
+ for (const [k, v] of Object.entries(args)) {
16
+ if (v === undefined)
17
+ continue;
18
+ let shown;
19
+ if (typeof v === 'string') {
20
+ // Long strings are a body, not an argument. Say how big instead.
21
+ shown = v.length > 80 ? `<${v.length} chars>` : v;
22
+ }
23
+ else if (typeof v === 'object' && v !== null) {
24
+ shown = Array.isArray(v) ? `<${v.length} items>` : '<object>';
25
+ }
26
+ else {
27
+ shown = String(v);
28
+ }
29
+ parts.push(`${k}=${shown}`);
30
+ }
31
+ const joined = parts.join(' ');
32
+ return redact(joined.length > MAX_ARG_CHARS ? `${joined.slice(0, MAX_ARG_CHARS)}…` : joined);
33
+ }
34
+ /** `~/.husk/audit/<computerId>.ndjson` -- one file per machine. */
35
+ export function auditPath(computerId) {
36
+ return join(paths().data, 'audit', `${computerId}.ndjson`);
37
+ }
38
+ /**
39
+ * Append one entry. Never throws.
40
+ *
41
+ * The caller is on the response path of a tool the model is waiting for, so
42
+ * this does not block it and cannot break it.
43
+ */
44
+ export async function recordAudit(entry) {
45
+ try {
46
+ const file = auditPath(entry.computerId);
47
+ await mkdir(dirname(file), { recursive: true });
48
+ await appendFile(file, `${JSON.stringify(entry)}\n`, 'utf8');
49
+ }
50
+ catch {
51
+ // An audit log that can break the thing it audits is worse than none.
52
+ }
53
+ }
54
+ /**
55
+ * Time a call and record it, whatever the outcome.
56
+ *
57
+ * Wrapping rather than two call sites so a future tool cannot be added and
58
+ * quietly skip the log -- which is how the MCP path came to have none.
59
+ */
60
+ export async function audited(meta, run, failed) {
61
+ const started = Date.now();
62
+ const base = {
63
+ computerId: meta.computerId,
64
+ via: meta.via,
65
+ tool: meta.tool,
66
+ args: summariseArgs(meta.args),
67
+ };
68
+ try {
69
+ const value = await run();
70
+ // A tool that reports failure in its result rather than by throwing --
71
+ // which is how MCP tools signal errors -- must not be logged as a success.
72
+ const why = failed?.(value);
73
+ void recordAudit({ ...base, at: new Date().toISOString(), ok: !why, durationMs: Date.now() - started, ...(why ? { error: why } : {}) });
74
+ return value;
75
+ }
76
+ catch (err) {
77
+ void recordAudit({
78
+ ...base,
79
+ at: new Date().toISOString(),
80
+ ok: false,
81
+ durationMs: Date.now() - started,
82
+ error: redact(String(err.message ?? err).split('\n')[0] ?? ''),
83
+ });
84
+ throw err;
85
+ }
86
+ }
87
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAwCnC,8EAA8E;AAC9E,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAA6B;IACzD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC9B,IAAI,KAAa,CAAC;QAClB,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC1B,iEAAiE;YACjE,KAAK,GAAG,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAC/C,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC;QAChE,CAAC;aAAM,CAAC;YACN,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IAC9B,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAC/F,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,UAAkB;IAC1C,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,SAAS,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAiB;IACjD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,sEAAsE;IACxE,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,IAAsF,EACtF,GAAqB,EACrB,MAAyC;IAEzC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG;QACX,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;KAC/B,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,GAAG,EAAE,CAAC;QAC1B,uEAAuE;QACvE,2EAA2E;QAC3E,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;QAC5B,KAAK,WAAW,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACxI,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,WAAW,CAAC;YACf,GAAG,IAAI;YACP,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,EAAE,EAAE,KAAK;YACT,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO;YAChC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAE,GAAa,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SAC1E,CAAC,CAAC;QACH,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,66 @@
1
+ import type { Computer } from './types/computer.js';
2
+ import type { NetworkPolicy } from './types/computer.js';
3
+ /**
4
+ * Browsing, from inside the computer.
5
+ *
6
+ * The point is not to reimplement a browser. The point is that the agent's
7
+ * browser, its terminal and its filesystem are all views of the *same machine*.
8
+ * A `fetch()` from the host process is a different machine with a different IP,
9
+ * a different DNS view and a different egress path -- so a page the human sees
10
+ * in the console is not necessarily the page the agent got, and a network policy
11
+ * scoped to the computer would not apply to it.
12
+ *
13
+ * Everything here runs as a script in the computer. It needs only python3 or
14
+ * curl, both of which the husk images and a stock WSL both have.
15
+ */
16
+ /**
17
+ * The serialisable half of a browse request -- exactly what goes over the wire.
18
+ *
19
+ * Split from `BrowseRequest` because the route schema is `.strict()`: a client
20
+ * that reused a type carrying `signal` would send it and get a 422.
21
+ */
22
+ export interface BrowseBody {
23
+ url: string;
24
+ /** Follow redirects. Defaults to true. */
25
+ follow?: boolean;
26
+ /** Give up after this many seconds. Defaults to 30. */
27
+ timeoutSec?: number;
28
+ /** Cap the extracted text. Defaults to 200 KB. */
29
+ maxBytes?: number;
30
+ }
31
+ export interface BrowseRequest extends BrowseBody {
32
+ signal?: AbortSignal;
33
+ }
34
+ export interface BrowseLink {
35
+ text: string;
36
+ href: string;
37
+ }
38
+ export interface BrowsePage {
39
+ /** The URL actually loaded, after redirects. */
40
+ url: string;
41
+ requestedUrl: string;
42
+ status: number;
43
+ contentType: string;
44
+ title: string;
45
+ /** Readable text, tags stripped, whitespace collapsed. */
46
+ text: string;
47
+ links: BrowseLink[];
48
+ /** Bytes actually read, i.e. after any truncation. */
49
+ bytes: number;
50
+ /** What the server claimed in Content-Length, when it said. Null otherwise. */
51
+ totalBytes: number | null;
52
+ truncated: boolean;
53
+ elapsedMs: number;
54
+ /** The tool that did the fetch, so the console can say so. */
55
+ via: 'python3' | 'curl';
56
+ }
57
+ /**
58
+ * Load a page from inside `computer`, honouring its network policy.
59
+ *
60
+ * The policy check happens here, on the parsed URL, rather than being left to
61
+ * the provider: the `local` provider cannot filter egress at the OS level, so
62
+ * this is the only place the declared policy becomes real for a fetch husk
63
+ * makes on the agent's behalf.
64
+ */
65
+ export declare function browseInComputer(computer: Computer, req: BrowseRequest, policy?: NetworkPolicy): Promise<BrowsePage>;
66
+ //# sourceMappingURL=browse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../src/browse.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,0CAA0C;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,GAAG,EAAE,SAAS,GAAG,MAAM,CAAC;CACzB;AAgFD;;;;;;;GAOG;AAEH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,aAAa,EAClB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CA0DrB"}
package/dist/browse.js ADDED
@@ -0,0 +1,217 @@
1
+ import { HuskError } from './errors.js';
2
+ import { assertUrlAllowed, ownsLoopback } from './net.js';
3
+ /** Written into the computer once, then reused. */
4
+ const FETCH_SCRIPT = String.raw `
5
+ import json, re, sys, html, urllib.request, urllib.error, urllib.parse, time
6
+
7
+ url, follow, timeout, max_bytes = sys.argv[1], sys.argv[2] == "1", float(sys.argv[3]), int(sys.argv[4])
8
+
9
+ class NoRedirect(urllib.request.HTTPRedirectHandler):
10
+ def redirect_request(self, *a, **k):
11
+ return None
12
+
13
+ opener = urllib.request.build_opener(*([] if follow else [NoRedirect]))
14
+ req = urllib.request.Request(url, headers={
15
+ "User-Agent": "husk-browser/0.1 (+https://husk.sh)",
16
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5",
17
+ "Accept-Language": "en",
18
+ })
19
+
20
+ started = time.time()
21
+ status, final, ctype, raw, declared = 0, url, "", b"", None
22
+ try:
23
+ with opener.open(req, timeout=timeout) as res:
24
+ status = res.status
25
+ final = res.geturl()
26
+ ctype = res.headers.get("Content-Type", "")
27
+ declared = res.headers.get("Content-Length")
28
+ raw = res.read(max_bytes + 1)
29
+ except urllib.error.HTTPError as e:
30
+ status, final, ctype = e.code, e.geturl(), e.headers.get("Content-Type", "")
31
+ raw = e.read(max_bytes + 1)
32
+ except Exception as e:
33
+ print(json.dumps({"error": str(e)}))
34
+ raise SystemExit(0)
35
+
36
+ truncated = len(raw) > max_bytes
37
+ raw = raw[:max_bytes]
38
+
39
+ charset = "utf-8"
40
+ m = re.search(r"charset=([\w-]+)", ctype, re.I)
41
+ if m:
42
+ charset = m.group(1)
43
+ body = raw.decode(charset, errors="replace")
44
+
45
+ title, text, links = "", "", []
46
+ if "html" in ctype.lower() or body.lstrip()[:15].lower().startswith(("<!doctype", "<html")):
47
+ t = re.search(r"<title[^>]*>(.*?)</title>", body, re.S | re.I)
48
+ title = html.unescape(t.group(1)).strip()[:300] if t else ""
49
+
50
+ for m in re.finditer(r'<a\s[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', body, re.S | re.I):
51
+ href = html.unescape(m.group(1)).strip()
52
+ if href.startswith(("#", "javascript:", "mailto:")):
53
+ continue
54
+ label = html.unescape(re.sub(r"<[^>]+>", " ", m.group(2)))
55
+ label = re.sub(r"\s+", " ", label).strip()
56
+ if not label:
57
+ continue
58
+ links.append({"text": label[:160], "href": urllib.parse.urljoin(final, href)})
59
+ if len(links) >= 300:
60
+ break
61
+
62
+ stripped = re.sub(r"(?is)<(script|style|noscript|template|svg)\b.*?</\1>", " ", body)
63
+ stripped = re.sub(r"(?i)<br\s*/?>|</p>|</div>|</li>|</h[1-6]>", "\n", stripped)
64
+ stripped = re.sub(r"<[^>]+>", " ", stripped)
65
+ text = html.unescape(stripped)
66
+ text = re.sub(r"[ \t\r\f\v]+", " ", text)
67
+ text = re.sub(r"\n\s*\n\s*\n+", "\n\n", text).strip()
68
+ else:
69
+ text = body
70
+
71
+ print(json.dumps({
72
+ "url": final, "status": status, "contentType": ctype, "title": title,
73
+ "text": text, "links": links, "bytes": len(raw), "truncated": truncated,
74
+ "totalBytes": int(declared) if declared and declared.isdigit() else None,
75
+ "elapsedMs": int((time.time() - started) * 1000),
76
+ }))
77
+ `;
78
+ const SCRIPT_PATH = '/tmp/.husk-browse.py';
79
+ /**
80
+ * Load a page from inside `computer`, honouring its network policy.
81
+ *
82
+ * The policy check happens here, on the parsed URL, rather than being left to
83
+ * the provider: the `local` provider cannot filter egress at the OS level, so
84
+ * this is the only place the declared policy becomes real for a fetch husk
85
+ * makes on the agent's behalf.
86
+ */
87
+ export async function browseInComputer(computer, req, policy) {
88
+ const effective = policy ?? computer.info.spec.network;
89
+ const parsed = assertUrlAllowed(req.url, effective, {
90
+ loopbackIsOwn: ownsLoopback(computer.info.provider),
91
+ });
92
+ const timeoutSec = req.timeoutSec ?? 30;
93
+ const maxBytes = req.maxBytes ?? 200 * 1024;
94
+ const follow = req.follow !== false;
95
+ const hasPython = await probe(computer, 'python3', req.signal);
96
+ if (!hasPython)
97
+ return await browseWithCurl(computer, parsed, { follow, timeoutSec, maxBytes, signal: req.signal });
98
+ await computer.writeFile(SCRIPT_PATH, FETCH_SCRIPT);
99
+ const result = await computer.exec({
100
+ cmd: ['python3', SCRIPT_PATH, parsed.toString(), follow ? '1' : '0', String(timeoutSec), String(maxBytes)],
101
+ timeoutSec: timeoutSec + 10,
102
+ maxOutputBytes: maxBytes + 64 * 1024,
103
+ ...(req.signal ? { signal: req.signal } : {}),
104
+ });
105
+ if (result.exitCode !== 0 && !result.stdout.trim()) {
106
+ throw new HuskError('E_EXEC_FAILED', `could not load ${parsed.hostname}`, {
107
+ hint: 'check the computer has egress with `husk exec <name> -- curl -sS -o /dev/null -w "%{http_code}" https://example.com`',
108
+ details: { stderr: result.stderr.slice(0, 500) },
109
+ });
110
+ }
111
+ let parsedOut;
112
+ try {
113
+ parsedOut = JSON.parse(lastJsonLine(result.stdout));
114
+ }
115
+ catch {
116
+ throw new HuskError('E_EXEC_FAILED', `the page fetcher returned something unreadable`, {
117
+ hint: 'this is a husk bug; the raw output is in details',
118
+ details: { stdout: result.stdout.slice(0, 500) },
119
+ });
120
+ }
121
+ if (typeof parsedOut.error === 'string') {
122
+ throw new HuskError('E_EXEC_FAILED', `could not load ${parsed.hostname}: ${parsedOut.error}`, {
123
+ hint: 'the machine reached the network but the request failed -- check the URL and the host',
124
+ });
125
+ }
126
+ return {
127
+ requestedUrl: parsed.toString(),
128
+ url: String(parsedOut.url ?? parsed.toString()),
129
+ status: Number(parsedOut.status ?? 0),
130
+ contentType: String(parsedOut.contentType ?? ''),
131
+ title: String(parsedOut.title ?? ''),
132
+ text: String(parsedOut.text ?? ''),
133
+ links: Array.isArray(parsedOut.links) ? parsedOut.links : [],
134
+ bytes: Number(parsedOut.bytes ?? 0),
135
+ totalBytes: typeof parsedOut.totalBytes === 'number' ? parsedOut.totalBytes : null,
136
+ truncated: Boolean(parsedOut.truncated),
137
+ elapsedMs: Number(parsedOut.elapsedMs ?? 0),
138
+ via: 'python3',
139
+ };
140
+ }
141
+ /** No python3: fall back to curl and return the body with no extraction. */
142
+ async function browseWithCurl(computer, parsed, opts) {
143
+ const started = Date.now();
144
+ const args = [
145
+ 'curl',
146
+ '-sS',
147
+ ...(opts.follow ? ['-L'] : []),
148
+ '-m',
149
+ String(opts.timeoutSec),
150
+ '-A',
151
+ 'husk-browser/0.1 (+https://husk.sh)',
152
+ '-w',
153
+ '\\nHUSK_META %{http_code} %{content_type} %{url_effective}',
154
+ parsed.toString(),
155
+ ];
156
+ const res = await computer.exec({
157
+ cmd: args,
158
+ timeoutSec: opts.timeoutSec + 10,
159
+ maxOutputBytes: opts.maxBytes + 8192,
160
+ ...(opts.signal ? { signal: opts.signal } : {}),
161
+ });
162
+ if (res.exitCode !== 0) {
163
+ throw new HuskError('E_EXEC_FAILED', `could not load ${parsed.hostname}`, {
164
+ hint: 'this machine has neither python3 nor curl, so there is nothing here to fetch with. ' +
165
+ 'Use a flavor whose image ships one -- `husk up <name> --flavor python` works today -- ' +
166
+ 'or point computer.image at your own. On the container providers `computer.packages` ' +
167
+ 'cannot help here: the root filesystem is mounted read-only on purpose, so no package ' +
168
+ 'manager can run.',
169
+ details: { stderr: res.stderr.slice(0, 500) },
170
+ });
171
+ }
172
+ const meta = /\nHUSK_META (\d+) (\S*) (\S+)\s*$/.exec(res.stdout);
173
+ const body = meta ? res.stdout.slice(0, meta.index) : res.stdout;
174
+ return {
175
+ requestedUrl: parsed.toString(),
176
+ url: meta?.[3] ?? parsed.toString(),
177
+ status: meta ? Number(meta[1]) : 0,
178
+ contentType: meta?.[2] ?? '',
179
+ title: '',
180
+ text: stripTags(body),
181
+ links: [],
182
+ bytes: Buffer.byteLength(body, 'utf8'),
183
+ totalBytes: null,
184
+ truncated: res.truncated,
185
+ elapsedMs: Date.now() - started,
186
+ via: 'curl',
187
+ };
188
+ }
189
+ async function probe(computer, bin, signal) {
190
+ const r = await computer
191
+ .exec({
192
+ cmd: `command -v ${bin} >/dev/null 2>&1 && echo yes || echo no`,
193
+ timeoutSec: 20,
194
+ ...(signal ? { signal } : {}),
195
+ })
196
+ .catch(() => undefined);
197
+ return r?.stdout.includes('yes') ?? false;
198
+ }
199
+ /** The script prints one JSON object last; shells sometimes prepend noise. */
200
+ function lastJsonLine(out) {
201
+ const lines = out.trimEnd().split('\n');
202
+ for (let i = lines.length - 1; i >= 0; i--) {
203
+ const line = lines[i].trim();
204
+ if (line.startsWith('{') && line.endsWith('}'))
205
+ return line;
206
+ }
207
+ return out.trim();
208
+ }
209
+ function stripTags(input) {
210
+ return input
211
+ .replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, ' ')
212
+ .replace(/<[^>]+>/g, ' ')
213
+ .replace(/[ \t]+/g, ' ')
214
+ .replace(/\n\s*\n\s*\n+/g, '\n\n')
215
+ .trim();
216
+ }
217
+ //# sourceMappingURL=browse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browse.js","sourceRoot":"","sources":["../src/browse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Where Husk keeps its state.
3
+ *
4
+ * Everything lives under one directory so a user can delete it in one move,
5
+ * back it up in one move, and see exactly what we are storing.
6
+ */
7
+ export interface HuskPaths {
8
+ /** Root: $HUSK_HOME, else ~/.husk */
9
+ root: string;
10
+ /** Registered husks, one directory per husk. */
11
+ husks: string;
12
+ /** Computer bookkeeping (metadata, not filesystems). */
13
+ computers: string;
14
+ /** Working directories for the `local` provider. */
15
+ workspaces: string;
16
+ /** Run transcripts and traces. */
17
+ runs: string;
18
+ /** Imported transcripts, cached. */
19
+ transcripts: string;
20
+ /** SQLite databases. */
21
+ data: string;
22
+ /** Downloaded assets, snapshot tarballs. */
23
+ cache: string;
24
+ /** Long-lived config: husk.config.json, credentials pointer. */
25
+ configFile: string;
26
+ /** Local overrides that must never be committed. */
27
+ envFile: string;
28
+ }
29
+ export declare function huskHome(): string;
30
+ export declare function paths(): HuskPaths;
31
+ /** Create every directory Husk expects. Safe to call repeatedly. */
32
+ export declare function ensurePaths(p?: HuskPaths): HuskPaths;
33
+ /** Global settings, read from ~/.husk/config.json and the environment. */
34
+ export interface HuskConfig {
35
+ /** Preferred computer provider. 'auto' picks the best available. */
36
+ provider: string;
37
+ /** Preferred model alias. 'auto' picks the best available. */
38
+ model: string;
39
+ /** Control-plane bind address for `husk serve`. */
40
+ host: string;
41
+ port: number;
42
+ /** Refuse to spend more than this on a single run, whatever the husk says. */
43
+ maxCostUsd: number;
44
+ /** Hard cap on concurrently running computers. */
45
+ maxComputers: number;
46
+ /** Send nothing anywhere except the model provider you configured. */
47
+ telemetry: false;
48
+ logLevel: string;
49
+ /** Registry of aliases the user has pinned, e.g. { fast: 'groq/llama-3.3-70b' }. */
50
+ modelAliases: Record<string, string>;
51
+ }
52
+ export declare const DEFAULT_CONFIG: HuskConfig;
53
+ /** Environment variable names Husk reads. Documented so `husk doctor` can list them. */
54
+ export declare const ENV_KEYS: {
55
+ readonly anthropic: "ANTHROPIC_API_KEY";
56
+ readonly openai: "OPENAI_API_KEY";
57
+ readonly google: "GOOGLE_API_KEY";
58
+ readonly groq: "GROQ_API_KEY";
59
+ readonly openrouter: "OPENROUTER_API_KEY";
60
+ readonly together: "TOGETHER_API_KEY";
61
+ readonly deepseek: "DEEPSEEK_API_KEY";
62
+ readonly mistral: "MISTRAL_API_KEY";
63
+ readonly cerebras: "CEREBRAS_API_KEY";
64
+ readonly ollamaHost: "OLLAMA_HOST";
65
+ readonly lmstudioHost: "LMSTUDIO_HOST";
66
+ readonly fly: "FLY_API_TOKEN";
67
+ readonly discord: "DISCORD_BOT_TOKEN";
68
+ readonly slack: "SLACK_BOT_TOKEN";
69
+ readonly telegram: "TELEGRAM_BOT_TOKEN";
70
+ };
71
+ export declare const HUSK_PORT_DEFAULT = 7377;
72
+ export declare const HUSK_USER_AGENT = "husk/0.1.0 (+https://husk.sh)";
73
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,wBAAgB,KAAK,IAAI,SAAS,CAcjC;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,CAAC,GAAE,SAAmB,GAAG,SAAS,CAK7D;AAED,0EAA0E;AAC1E,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,SAAS,EAAE,KAAK,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,eAAO,MAAM,cAAc,EAAE,UAU5B,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;CAgBX,CAAC;AAEX,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,eAAe,kCAAkC,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,61 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+ export function huskHome() {
5
+ return process.env.HUSK_HOME ? resolve(process.env.HUSK_HOME) : join(homedir(), '.husk');
6
+ }
7
+ export function paths() {
8
+ const root = huskHome();
9
+ return {
10
+ root,
11
+ husks: join(root, 'husks'),
12
+ computers: join(root, 'computers'),
13
+ workspaces: join(root, 'workspaces'),
14
+ runs: join(root, 'runs'),
15
+ transcripts: join(root, 'transcripts'),
16
+ data: join(root, 'data'),
17
+ cache: join(root, 'cache'),
18
+ configFile: join(root, 'config.json'),
19
+ envFile: join(root, '.env'),
20
+ };
21
+ }
22
+ /** Create every directory Husk expects. Safe to call repeatedly. */
23
+ export function ensurePaths(p = paths()) {
24
+ for (const dir of [p.root, p.husks, p.computers, p.workspaces, p.runs, p.transcripts, p.data, p.cache]) {
25
+ if (!existsSync(dir))
26
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
27
+ }
28
+ return p;
29
+ }
30
+ export const DEFAULT_CONFIG = {
31
+ provider: 'auto',
32
+ model: 'auto',
33
+ host: '127.0.0.1',
34
+ port: 7377,
35
+ maxCostUsd: 5,
36
+ maxComputers: 8,
37
+ telemetry: false,
38
+ logLevel: 'info',
39
+ modelAliases: {},
40
+ };
41
+ /** Environment variable names Husk reads. Documented so `husk doctor` can list them. */
42
+ export const ENV_KEYS = {
43
+ anthropic: 'ANTHROPIC_API_KEY',
44
+ openai: 'OPENAI_API_KEY',
45
+ google: 'GOOGLE_API_KEY',
46
+ groq: 'GROQ_API_KEY',
47
+ openrouter: 'OPENROUTER_API_KEY',
48
+ together: 'TOGETHER_API_KEY',
49
+ deepseek: 'DEEPSEEK_API_KEY',
50
+ mistral: 'MISTRAL_API_KEY',
51
+ cerebras: 'CEREBRAS_API_KEY',
52
+ ollamaHost: 'OLLAMA_HOST',
53
+ lmstudioHost: 'LMSTUDIO_HOST',
54
+ fly: 'FLY_API_TOKEN',
55
+ discord: 'DISCORD_BOT_TOKEN',
56
+ slack: 'SLACK_BOT_TOKEN',
57
+ telegram: 'TELEGRAM_BOT_TOKEN',
58
+ };
59
+ export const HUSK_PORT_DEFAULT = 7377;
60
+ export const HUSK_USER_AGENT = 'husk/0.1.0 (+https://husk.sh)';
61
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AA+BhD,MAAM,UAAU,QAAQ;IACtB,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,KAAK;IACnB,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;IACxB,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC;QAClC,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;QACpC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;QACxB,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC;QACtC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;QACxB,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC;QAC1B,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC;QACrC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;KAC5B,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,WAAW,CAAC,IAAe,KAAK,EAAE;IAChD,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QACvG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAsBD,MAAM,CAAC,MAAM,cAAc,GAAe;IACxC,QAAQ,EAAE,MAAM;IAChB,KAAK,EAAE,MAAM;IACb,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,CAAC;IACb,YAAY,EAAE,CAAC;IACf,SAAS,EAAE,KAAK;IAChB,QAAQ,EAAE,MAAM;IAChB,YAAY,EAAE,EAAE;CACjB,CAAC;AAEF,wFAAwF;AACxF,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,SAAS,EAAE,mBAAmB;IAC9B,MAAM,EAAE,gBAAgB;IACxB,MAAM,EAAE,gBAAgB;IACxB,IAAI,EAAE,cAAc;IACpB,UAAU,EAAE,oBAAoB;IAChC,QAAQ,EAAE,kBAAkB;IAC5B,QAAQ,EAAE,kBAAkB;IAC5B,OAAO,EAAE,iBAAiB;IAC1B,QAAQ,EAAE,kBAAkB;IAC5B,UAAU,EAAE,aAAa;IACzB,YAAY,EAAE,eAAe;IAC7B,GAAG,EAAE,eAAe;IACpB,OAAO,EAAE,mBAAmB;IAC5B,KAAK,EAAE,iBAAiB;IACxB,QAAQ,EAAE,oBAAoB;CACtB,CAAC;AAEX,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,+BAA+B,CAAC"}