@alisio/core 0.1.0-alpha.20 → 0.1.0-alpha.22

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 (104) hide show
  1. package/dist/agents/active.d.ts +19 -0
  2. package/dist/agents/active.js +41 -0
  3. package/dist/agents/capabilities.d.ts +26 -0
  4. package/dist/agents/capabilities.js +120 -0
  5. package/dist/agents/create-agent-skill.d.ts +11 -0
  6. package/dist/agents/create-agent-skill.js +60 -0
  7. package/dist/agents/definitions.d.ts +88 -0
  8. package/dist/agents/definitions.js +404 -0
  9. package/dist/agents/draft.d.ts +41 -0
  10. package/dist/agents/draft.js +133 -0
  11. package/dist/agents/service.d.ts +57 -0
  12. package/dist/agents/service.js +86 -0
  13. package/dist/agents/templates.d.ts +8 -0
  14. package/dist/agents/templates.js +102 -0
  15. package/dist/analysis/capabilities.d.ts +48 -0
  16. package/dist/analysis/capabilities.js +90 -0
  17. package/dist/analysis/data/csv.d.ts +46 -0
  18. package/dist/analysis/data/csv.js +240 -0
  19. package/dist/analysis/data/datasets.d.ts +132 -0
  20. package/dist/analysis/data/datasets.js +441 -0
  21. package/dist/analysis/data/describe.d.ts +27 -0
  22. package/dist/analysis/data/describe.js +103 -0
  23. package/dist/analysis/data/engine-client.d.ts +58 -0
  24. package/dist/analysis/data/engine-client.js +304 -0
  25. package/dist/analysis/data/engine-entry.d.ts +1 -0
  26. package/dist/analysis/data/engine-entry.js +59 -0
  27. package/dist/analysis/data/engine-source.d.ts +4 -0
  28. package/dist/analysis/data/engine-source.js +6 -0
  29. package/dist/analysis/data/engine-types.d.ts +86 -0
  30. package/dist/analysis/data/engine-types.js +7 -0
  31. package/dist/analysis/data/infer.d.ts +42 -0
  32. package/dist/analysis/data/infer.js +118 -0
  33. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  34. package/dist/analysis/data/ingest-worker.js +365 -0
  35. package/dist/analysis/data/json.d.ts +19 -0
  36. package/dist/analysis/data/json.js +44 -0
  37. package/dist/analysis/data/query-worker.d.ts +12 -0
  38. package/dist/analysis/data/query-worker.js +75 -0
  39. package/dist/analysis/data/rows.d.ts +39 -0
  40. package/dist/analysis/data/rows.js +120 -0
  41. package/dist/analysis/data/sql-guard.d.ts +20 -0
  42. package/dist/analysis/data/sql-guard.js +129 -0
  43. package/dist/analysis/data/stats.d.ts +18 -0
  44. package/dist/analysis/data/stats.js +77 -0
  45. package/dist/analysis/install-hints.d.ts +33 -0
  46. package/dist/analysis/install-hints.js +144 -0
  47. package/dist/analysis/janitor.d.ts +60 -0
  48. package/dist/analysis/janitor.js +264 -0
  49. package/dist/analysis/jobs.d.ts +94 -0
  50. package/dist/analysis/jobs.js +165 -0
  51. package/dist/analysis/oci.d.ts +129 -0
  52. package/dist/analysis/oci.js +330 -0
  53. package/dist/analysis/python/sources.d.ts +2 -0
  54. package/dist/analysis/python/sources.js +10 -0
  55. package/dist/analysis/requirements.d.ts +32 -0
  56. package/dist/analysis/requirements.js +73 -0
  57. package/dist/analysis/rerun.d.ts +40 -0
  58. package/dist/analysis/rerun.js +136 -0
  59. package/dist/analysis/runtime-manager.d.ts +146 -0
  60. package/dist/analysis/runtime-manager.js +472 -0
  61. package/dist/analysis/status.d.ts +21 -0
  62. package/dist/analysis/status.js +52 -0
  63. package/dist/application.d.ts +48 -0
  64. package/dist/application.js +228 -0
  65. package/dist/artifacts/kinds.d.ts +35 -0
  66. package/dist/artifacts/kinds.js +177 -0
  67. package/dist/artifacts/publisher.d.ts +7 -0
  68. package/dist/artifacts/publisher.js +29 -0
  69. package/dist/artifacts/store.d.ts +119 -0
  70. package/dist/artifacts/store.js +581 -0
  71. package/dist/commands/builtins.js +13 -0
  72. package/dist/commands/catalog.js +13 -1
  73. package/dist/config.d.ts +102 -0
  74. package/dist/config.js +167 -6
  75. package/dist/core/contracts.d.ts +69 -1
  76. package/dist/core/runner.d.ts +34 -4
  77. package/dist/core/runner.js +388 -130
  78. package/dist/index.d.ts +30 -5
  79. package/dist/index.js +29 -4
  80. package/dist/mcp/rich.js +4 -0
  81. package/dist/plugins/host.js +14 -6
  82. package/dist/runtime/blobs.d.ts +22 -0
  83. package/dist/runtime/blobs.js +84 -1
  84. package/dist/runtime/ids.d.ts +8 -0
  85. package/dist/runtime/ids.js +32 -0
  86. package/dist/runtime/paths.d.ts +5 -0
  87. package/dist/runtime/paths.js +6 -0
  88. package/dist/runtime/process.d.ts +12 -0
  89. package/dist/runtime/process.js +64 -4
  90. package/dist/runtime/sqlite.d.ts +2 -0
  91. package/dist/runtime/sqlite.js +18 -0
  92. package/dist/runtime/store.d.ts +5 -0
  93. package/dist/runtime/store.js +75 -0
  94. package/dist/runtime/zip.d.ts +24 -0
  95. package/dist/runtime/zip.js +126 -0
  96. package/dist/sessions/children.js +4 -0
  97. package/dist/tools/analysis.d.ts +37 -0
  98. package/dist/tools/analysis.js +572 -0
  99. package/dist/tools/artifacts.d.ts +14 -0
  100. package/dist/tools/artifacts.js +143 -0
  101. package/dist/tools/data.d.ts +7 -0
  102. package/dist/tools/data.js +160 -0
  103. package/dist/tools/execute.js +6 -2
  104. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,9 +1,30 @@
1
1
  /** @alisio/core public API: embed the Alisio agent programmatically. */
2
2
  export * from "@alisio/sdk";
3
- export { activeAgentCatalog, agentCatalogFromState, agentPickerItems, agentRunOptions, BUILTIN_AGENTS, DEFAULT_AGENT_ID, mainAgentFromRecord, resolveActiveAgent, } from "./agents/active.js";
3
+ export { AGENT_COMMAND_PREFIX, activeAgentCatalog, agentCatalogFromState, agentCommandDescriptors, agentCommandName, agentIdFromCommand, agentPickerItems, agentRunOptions, BUILTIN_AGENTS, DEFAULT_AGENT_ID, mainAgentFromRecord, resolveActiveAgent, } from "./agents/active.js";
4
+ export { agentModelCapabilities, agentModelLabels, agentModelOptions, DEFAULT_EFFORT_LEVELS, fitAgentSettings, } from "./agents/capabilities.js";
5
+ export { AGENT_AUTHORING_SKILL_NAMES, CREATE_AGENT_SKILL, } from "./agents/create-agent-skill.js";
6
+ export { AGENT_ID_PATTERN, AGENT_LIMITS, AGENT_REASONING_SUMMARIES, AGENT_TEXT_FORMATS, AGENT_VERBOSITIES, AgentNotFoundError, AgentScopeStore, agentIdFromName, agentScopeDir, parseAgentFile, RESERVED_AGENT_IDS, slugify, validateAgentDefinitionInput, } from "./agents/definitions.js";
7
+ export { AGENT_DRAFT_INSTRUCTIONS, AGENT_DRAFT_MAX_DESCRIPTION, agentAuthoringGuidance, agentDraftRequest, generateAgentDraft, parseAgentDraft, } from "./agents/draft.js";
8
+ export { AgentDefinitionService, mergeAgentScopes, } from "./agents/service.js";
9
+ export { AGENT_TEMPLATES, agentTemplate } from "./agents/templates.js";
10
+ export { CapabilityGrants, toGrantWire, } from "./analysis/capabilities.js";
11
+ export { CsvParser, decodeTextBytes, detectDelimiter } from "./analysis/data/csv.js";
12
+ export { DatasetService, DEFAULT_DATASET_LIMITS, datasetFormat, PageParamError, toDatasetRef, XLSX_REMEDY, } from "./analysis/data/datasets.js";
13
+ export { DataEngine, DataError, } from "./analysis/data/engine-client.js";
14
+ export { guardSql } from "./analysis/data/sql-guard.js";
15
+ export { detectHintHost, installHintsMarkdown, installHintsText, parseOsRelease, pythonInstallHints, } from "./analysis/install-hints.js";
16
+ export { AnalysisJanitor, SWEEP_INTERVAL_MS, } from "./analysis/janitor.js";
17
+ export { AnalysisJobs, } from "./analysis/jobs.js";
18
+ export { containerName, OciRuntime, ociRunArgs, parseDigests, } from "./analysis/oci.js";
19
+ export { AnalysisRerun, } from "./analysis/rerun.js";
20
+ export { AnalysisRuntimeManager, ExtrasInstallError, installPreview, setupCommand, venvInterpreter, } from "./analysis/runtime-manager.js";
21
+ export { analysisStatus } from "./analysis/status.js";
4
22
  export { createApplication, enabledBuiltins, } from "./application.js";
23
+ export { classifyArtifact, extensionOf, fileType as artifactFileType, isPreviewable, KIND_LABELS, viewerContentType, } from "./artifacts/kinds.js";
24
+ export { createArtifactPublisher } from "./artifacts/publisher.js";
25
+ export { ArtifactRejected, ArtifactStore, readHead as readArtifactHead, slugify as artifactSlug, toRef, } from "./artifacts/store.js";
5
26
  export { BUILTIN_COMMANDS, CommandCatalog, } from "./commands/catalog.js";
6
- export { configFile, configHome, configSchema, isSettableSettingKey, loadConfig, loadConfigWithProvenance, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.js";
27
+ export { configFile, configHome, configSchema, isSettableSettingKey, loadConfig, loadConfigWithProvenance, OCI_IMAGE_PATTERN, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.js";
7
28
  export { checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.js";
8
29
  export { ToolRegistry } from "./core/registry.js";
9
30
  export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
@@ -19,16 +40,20 @@ export { ProjectContext, } from "./resources/context.js";
19
40
  export { expandSlashPrompt, loadPromptTemplates, parsePromptTemplate, promptSources, renderPromptTemplate, splitArguments, } from "./resources/prompts.js";
20
41
  export { Skills, skillRoots, } from "./resources/skills.js";
21
42
  export { PathAccess, } from "./runtime/access.js";
22
- export { BlobStore } from "./runtime/blobs.js";
43
+ export { BlobStore, BlobTooLarge } from "./runtime/blobs.js";
23
44
  export { clipLines, unifiedPatch } from "./runtime/diff.js";
24
45
  export { exists, fileSize, readHead, readJson, readText, which } from "./runtime/fs.js";
46
+ export { newId, ulid } from "./runtime/ids.js";
25
47
  export { defaultGlobalRoots, isPathSpec, PLUGIN_KEYWORD, resolvePluginSpec, } from "./runtime/modules.js";
26
- export { findWorkspace, inside, outsideRootsMessage, safePath } from "./runtime/paths.js";
48
+ export { findWorkspace, inside, outsideRootsMessage, safePath, workspaceKey, } from "./runtime/paths.js";
27
49
  export { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "./runtime/process.js";
28
50
  export { isSqliteExperimentalWarning, openDatabase } from "./runtime/sqlite.js";
29
51
  export { SQLiteStore } from "./runtime/store.js";
52
+ export { crc32, writeZip } from "./runtime/zip.js";
30
53
  export { ChildSessions } from "./sessions/children.js";
31
54
  export { formatSideQuestion, SIDE_QUESTION_DESCRIPTION, SIDE_QUESTION_HISTORY_LIMIT, SIDE_QUESTION_INSTRUCTIONS, SIDE_QUESTION_MAX_CHARS, SIDE_QUESTION_USAGE, SIDE_QUESTIONS_NAMESPACE, SideQuestions, sideTranscript, } from "./sessions/side-questions.js";
32
55
  export { composeSideBySide, DefaultAlisioMascot, DefaultStartupScreen, infoSection, mascotSection, padEnd, pluginsSection, renderStartup, sanitizeLine, shortenPath, startupTips, stripAnsi, tipsSection, titleSection, truncate, visibleWidth, welcomeSection, } from "./startup/index.js";
56
+ export { formatBytes, NOT_SANDBOXED } from "./tools/analysis.js";
57
+ export { ARTIFACT_READ_MAX, exportedPaths } from "./tools/artifacts.js";
33
58
  export { hash, objectSchema, registerStandard } from "./tools/standard.js";
34
59
  export { getTrust, hashProjectConfig, hasProjectResources, listTrust, resolveTrust, revokeTrust, setTrust, trustStorePath, } from "./trust.js";
package/dist/mcp/rich.js CHANGED
@@ -227,6 +227,10 @@ export function renderUiBlockText(block) {
227
227
  block.title ? `[progress: ${block.title}]` : "[progress]",
228
228
  ...block.steps.map((step) => `- [${step.status}] ${step.label}${step.detail ? ` (${step.detail})` : ""}`),
229
229
  ].join("\n");
230
+ case "artifact": {
231
+ const a = block.artifact;
232
+ return `[artifact: ${a.fileName} · ${a.kind} · ${a.bytes} bytes · ${a.id}${a.partial ? " · partial" : ""}${a.status !== "ready" ? ` · ${a.status}` : ""}]`;
233
+ }
230
234
  default: {
231
235
  // A kind this build does not know (e.g. persisted by a newer Alisio): keep it readable.
232
236
  const kind = String(block.kind ?? "unknown");
@@ -203,12 +203,20 @@ export class PluginHost {
203
203
  register: (tool) => {
204
204
  const registered = builtin
205
205
  ? tool
206
- : {
207
- ...tool,
208
- name: `${pluginPrefix(plugin.id)}_${tool.name}`,
209
- // Only built-ins may claim the always-allowed internal effect.
210
- ...(tool.effect === "internal" ? { effect: "external" } : {}),
211
- };
206
+ : (() => {
207
+ // Only built-ins may claim the always-allowed internal effect, a capability
208
+ // (finer than the effect) or the artifact publisher (decision D12, v1).
209
+ const { capability: _capability, ...rest } = tool;
210
+ return {
211
+ ...rest,
212
+ name: `${pluginPrefix(plugin.id)}_${tool.name}`,
213
+ ...(tool.effect === "internal" ? { effect: "external" } : {}),
214
+ execute: (input, context) => {
215
+ const { artifacts: _artifacts, approveInstall: _install, ...narrowed } = context;
216
+ return tool.execute(input, narrowed);
217
+ },
218
+ };
219
+ })();
212
220
  const unregister = this.registry.register(registered);
213
221
  this.toolOwners.set(registered.name, plugin.id);
214
222
  return track(() => {
@@ -1,4 +1,9 @@
1
1
  import type { Attachment, BlobRef, SqlDatabase } from "@alisio/sdk";
2
+ /** A streamed blob exceeded its size limit. */
3
+ export declare class BlobTooLarge extends Error {
4
+ readonly limit: number;
5
+ constructor(limit: number);
6
+ }
2
7
  export interface BlobStoreOptions {
3
8
  /** Directory that holds `sha256/…` (by default `<state home>/blobs`). */
4
9
  root: string;
@@ -17,6 +22,23 @@ export declare class BlobStore {
17
22
  width?: number;
18
23
  height?: number;
19
24
  }): BlobRef;
25
+ /**
26
+ * Stores a byte stream (an upload too large to hold in memory, e.g. a dataset) once per content
27
+ * hash, hashing while it writes. Fails with `BlobTooLarge` as soon as `maxBytes` is exceeded and
28
+ * leaves nothing behind. The stored type is the declared one; images stay the only blobs that
29
+ * `attachment()` and the blob route serve.
30
+ */
31
+ putStream(stream: AsyncIterable<Uint8Array>, mimeType: string, options: {
32
+ maxBytes: number;
33
+ }): Promise<BlobRef>;
34
+ /**
35
+ * Retention: deletes the bytes and the row of a blob that is not an image (an uploaded dataset
36
+ * original). Images are referenced by messages and are never removed here. Returns the freed
37
+ * bytes (0 when the blob is unknown, an image or already gone).
38
+ */
39
+ removeUpload(hash: string): number;
40
+ /** Hashes of non-image blobs created before `cutoff` (epoch ms): uploaded dataset originals. */
41
+ uploadsBefore(cutoff: number): string[];
20
42
  get(hash: string): {
21
43
  bytes: Buffer;
22
44
  mimeType: string;
@@ -8,9 +8,18 @@
8
8
  * Persisted messages therefore still carry base64 (as legacy inline attachments do).
9
9
  */
10
10
  import { createHash, randomUUID } from "node:crypto";
11
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
11
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
12
12
  import { join } from "node:path";
13
+ import { pipeline } from "node:stream/promises";
13
14
  const HASH = /^[0-9a-f]{64}$/;
15
+ /** A streamed blob exceeded its size limit. */
16
+ export class BlobTooLarge extends Error {
17
+ limit;
18
+ constructor(limit) {
19
+ super(`The upload exceeds ${limit} bytes`);
20
+ this.limit = limit;
21
+ }
22
+ }
14
23
  export class BlobStore {
15
24
  options;
16
25
  constructor(options) {
@@ -61,6 +70,80 @@ export class BlobStore {
61
70
  ...(row?.height != null ? { height: Number(row.height) } : {}),
62
71
  };
63
72
  }
73
+ /**
74
+ * Stores a byte stream (an upload too large to hold in memory, e.g. a dataset) once per content
75
+ * hash, hashing while it writes. Fails with `BlobTooLarge` as soon as `maxBytes` is exceeded and
76
+ * leaves nothing behind. The stored type is the declared one; images stay the only blobs that
77
+ * `attachment()` and the blob route serve.
78
+ */
79
+ async putStream(stream, mimeType, options) {
80
+ const mime = mimeType.trim();
81
+ if (!mime || mime.length > 255)
82
+ throw new Error("A blob needs a MIME type");
83
+ const dir = join(this.options.root, "sha256");
84
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
85
+ const temp = join(dir, `.tmp-${randomUUID()}`);
86
+ const hash = createHash("sha256");
87
+ let size = 0;
88
+ try {
89
+ await pipeline(stream, async function* (source) {
90
+ for await (const chunk of source) {
91
+ size += chunk.byteLength;
92
+ if (size > options.maxBytes)
93
+ throw new BlobTooLarge(options.maxBytes);
94
+ hash.update(chunk);
95
+ yield chunk;
96
+ }
97
+ }, createWriteStream(temp, { mode: 0o600, flags: "wx" }));
98
+ const digest = hash.digest("hex");
99
+ const file = this.path(digest);
100
+ const fileDir = join(dir, digest.slice(0, 2));
101
+ mkdirSync(fileDir, { recursive: true, mode: 0o700 });
102
+ chmodSync(fileDir, 0o700);
103
+ if (existsSync(file))
104
+ rmSync(temp, { force: true });
105
+ else
106
+ renameSync(temp, file);
107
+ this.options.db
108
+ .prepare("INSERT OR IGNORE INTO blobs(hash,mime,size,width,height,created_at) VALUES(?,?,?,?,?,?)")
109
+ .run(digest, mime, statSync(file).size, null, null, Date.now());
110
+ const row = this.row(digest);
111
+ return { hash: digest, mimeType: row?.mime ?? mime, bytes: statSync(file).size };
112
+ }
113
+ catch (error) {
114
+ rmSync(temp, { force: true });
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * Retention: deletes the bytes and the row of a blob that is not an image (an uploaded dataset
120
+ * original). Images are referenced by messages and are never removed here. Returns the freed
121
+ * bytes (0 when the blob is unknown, an image or already gone).
122
+ */
123
+ removeUpload(hash) {
124
+ if (!HASH.test(hash))
125
+ return 0;
126
+ const row = this.row(hash);
127
+ if (row?.mime.startsWith("image/"))
128
+ return 0;
129
+ const file = this.path(hash);
130
+ let size = 0;
131
+ try {
132
+ size = statSync(file).size;
133
+ rmSync(file, { force: true });
134
+ }
135
+ catch {
136
+ /* Already gone. */
137
+ }
138
+ this.options.db.prepare("DELETE FROM blobs WHERE hash=?").run(hash);
139
+ return size;
140
+ }
141
+ /** Hashes of non-image blobs created before `cutoff` (epoch ms): uploaded dataset originals. */
142
+ uploadsBefore(cutoff) {
143
+ return this.options.db
144
+ .prepare("SELECT hash FROM blobs WHERE mime NOT LIKE 'image/%' AND created_at<?")
145
+ .all(cutoff).map((r) => r.hash);
146
+ }
64
147
  get(hash) {
65
148
  if (!HASH.test(hash))
66
149
  return undefined;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A ULID (26 characters, lexicographically sortable by creation time): 48 bits of milliseconds
3
+ * and 80 random bits. No dependency and no monotonic counter; two ids created in the same
4
+ * millisecond sort arbitrarily, which is fine for row ids and folder names.
5
+ */
6
+ export declare function ulid(now?: number): string;
7
+ /** A prefixed id such as `art_01J…` or `exec_01J…`. */
8
+ export declare const newId: (prefix: string, now?: number) => string;
@@ -0,0 +1,32 @@
1
+ import { randomBytes } from "node:crypto";
2
+ /** Crockford base32 (no I, L, O, U), the ULID alphabet. */
3
+ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
4
+ /**
5
+ * A ULID (26 characters, lexicographically sortable by creation time): 48 bits of milliseconds
6
+ * and 80 random bits. No dependency and no monotonic counter; two ids created in the same
7
+ * millisecond sort arbitrarily, which is fine for row ids and folder names.
8
+ */
9
+ export function ulid(now = Date.now()) {
10
+ let time = "";
11
+ let t = Math.max(0, Math.floor(now));
12
+ for (let i = 0; i < 10; i++) {
13
+ time = ALPHABET[t % 32] + time;
14
+ t = Math.floor(t / 32);
15
+ }
16
+ const bytes = randomBytes(10);
17
+ let random = "";
18
+ // 80 bits → 16 characters of 5 bits each.
19
+ let buffer = 0, bits = 0;
20
+ for (const byte of bytes) {
21
+ buffer = (buffer << 8) | byte;
22
+ bits += 8;
23
+ while (bits >= 5) {
24
+ bits -= 5;
25
+ random += ALPHABET[(buffer >> bits) & 31];
26
+ }
27
+ buffer &= (1 << bits) - 1;
28
+ }
29
+ return time + random;
30
+ }
31
+ /** A prefixed id such as `art_01J…` or `exec_01J…`. */
32
+ export const newId = (prefix, now) => `${prefix}_${ulid(now)}`;
@@ -8,3 +8,8 @@ export declare function outsideRootsMessage(target: string, reason?: string): st
8
8
  /** Mediated path policy; not an OS sandbox and not race-proof against hostile concurrent processes. */
9
9
  export declare function safePath(root: string, path: string): Promise<string>;
10
10
  export declare function findWorkspace(cwd: string): Promise<string>;
11
+ /**
12
+ * Opaque, stable key of a workspace path: the first 16 hex characters of its sha256. Core names
13
+ * per-workspace folders with it and the server exposes it as the web `workspaceId`.
14
+ */
15
+ export declare const workspaceKey: (path: string) => string;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { lstat, realpath } from "node:fs/promises";
2
3
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
4
  export function inside(root, path) {
@@ -50,3 +51,8 @@ export async function findWorkspace(cwd) {
50
51
  path = dirname(path);
51
52
  }
52
53
  }
54
+ /**
55
+ * Opaque, stable key of a workspace path: the first 16 hex characters of its sha256. Core names
56
+ * per-workspace folders with it and the server exposes it as the web `workspaceId`.
57
+ */
58
+ export const workspaceKey = (path) => createHash("sha256").update(path).digest("hex").slice(0, 16);
@@ -22,4 +22,16 @@ export declare function runProcess(command: string, args: string[], options: {
22
22
  onData?: (chunk: string) => void;
23
23
  /** On cancellation: SIGTERM the process group, then SIGKILL after this grace (default 5s). */
24
24
  killGraceMs?: number;
25
+ /**
26
+ * What happens when a stream exceeds `maxBytes`: `kill` (default) stops the process; `truncate`
27
+ * lets it run and keeps only the LAST `maxBytes` of each stream (the result is `truncated`).
28
+ */
29
+ onOverflow?: "kill" | "truncate";
30
+ /** Full stdout/stderr copies (up to `maxLogBytes` each), independent of `maxBytes`. */
31
+ logFiles?: {
32
+ stdout: string;
33
+ stderr: string;
34
+ };
35
+ /** Cap of each log file (default 10 MiB); a marker line notes the cut. */
36
+ maxLogBytes?: number;
25
37
  }): Promise<ProcessResult>;
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { createWriteStream } from "node:fs";
2
3
  /**
3
4
  * Detect a spawn failure caused by a missing executable (`ENOENT`), so tools can explain how to
4
5
  * install the dependency instead of surfacing the raw OS error.
@@ -76,19 +77,77 @@ export async function runProcess(command, args, options) {
76
77
  terminate();
77
78
  let size = 0, truncated = false;
78
79
  const max = options.maxBytes ?? 32_000;
79
- const consume = (stream) => new Promise((resolveText, reject) => {
80
+ const tailMode = options.onOverflow === "truncate";
81
+ const maxLog = options.maxLogBytes ?? 10 * 1024 * 1024;
82
+ const logs = [];
83
+ const logger = (path) => {
84
+ if (!path)
85
+ return undefined;
86
+ const out = createWriteStream(path, { flags: "w" });
87
+ out.on("error", () => {
88
+ /* A log write failure never fails the process run. */
89
+ });
90
+ logs.push(out);
91
+ let written = 0, cut = false;
92
+ return (chunk) => {
93
+ if (cut)
94
+ return;
95
+ const room = maxLog - written;
96
+ if (chunk.byteLength > room) {
97
+ out.write(chunk.subarray(0, Math.max(0, room)));
98
+ out.write(Buffer.from("\n[log truncated]\n"));
99
+ cut = true;
100
+ written = maxLog;
101
+ return;
102
+ }
103
+ out.write(chunk);
104
+ written += chunk.byteLength;
105
+ };
106
+ };
107
+ const consume = (stream, logPath) => new Promise((resolveText, reject) => {
80
108
  const decoder = new TextDecoder();
109
+ const log = logger(logPath);
81
110
  let text = "", done = false;
111
+ // Tail mode keeps raw buffers and decodes once at the end (a cut may split a character).
112
+ const tail = [];
113
+ let tailBytes = 0;
82
114
  const finish = () => {
83
115
  if (done)
84
116
  return;
85
117
  done = true;
86
- text += decoder.decode();
118
+ if (tailMode) {
119
+ const joined = Buffer.concat(tail);
120
+ text = new TextDecoder().decode(joined).replace(/^\uFFFD+/, "");
121
+ }
122
+ else
123
+ text += decoder.decode();
87
124
  resolveText(text);
88
125
  };
89
126
  stream.on("data", (value) => {
90
127
  if (done)
91
128
  return;
129
+ log?.(value);
130
+ if (tailMode) {
131
+ const live = decoder.decode(value, { stream: true });
132
+ if (live)
133
+ options.onData?.(live);
134
+ tail.push(value);
135
+ tailBytes += value.byteLength;
136
+ while (tailBytes > max && tail.length) {
137
+ const first = tail[0];
138
+ const excess = tailBytes - max;
139
+ if (first.byteLength <= excess) {
140
+ tail.shift();
141
+ tailBytes -= first.byteLength;
142
+ }
143
+ else {
144
+ tail[0] = first.subarray(excess);
145
+ tailBytes -= excess;
146
+ }
147
+ truncated = true;
148
+ }
149
+ return;
150
+ }
92
151
  const remaining = Math.max(0, max - size), slice = value.subarray(0, remaining);
93
152
  size += slice.byteLength;
94
153
  const chunk = decoder.decode(slice, { stream: true });
@@ -107,8 +166,8 @@ export async function runProcess(command, args, options) {
107
166
  });
108
167
  try {
109
168
  const [stdout, stderr, exitCode] = await Promise.all([
110
- consume(child.stdout),
111
- consume(child.stderr),
169
+ consume(child.stdout, options.logFiles?.stdout),
170
+ consume(child.stderr, options.logFiles?.stderr),
112
171
  exited,
113
172
  ]);
114
173
  signal.throwIfAborted();
@@ -118,5 +177,6 @@ export async function runProcess(command, args, options) {
118
177
  signal.removeEventListener("abort", terminate);
119
178
  clearTimeout(graceTimer);
120
179
  kill();
180
+ await Promise.all(logs.map((out) => new Promise((resolveLog) => out.end(() => resolveLog()))));
121
181
  }
122
182
  }
@@ -1,3 +1,5 @@
1
1
  import type { SqlDatabase } from "@alisio/sdk";
2
2
  export declare function isSqliteExperimentalWarning(message: string, type?: string): boolean;
3
+ /** Opens an existing database file read-only (no directory or file is created). */
4
+ export declare function openReadOnlyDatabase(path: string): SqlDatabase;
3
5
  export declare function openDatabase(path: string): SqlDatabase;
@@ -34,6 +34,24 @@ function sqlite() {
34
34
  return module;
35
35
  }
36
36
  const plain = (row) => row === undefined || row === null ? undefined : { ...row };
37
+ /** Opens an existing database file read-only (no directory or file is created). */
38
+ export function openReadOnlyDatabase(path) {
39
+ const db = new (sqlite().DatabaseSync)(path, { readOnly: true });
40
+ db.exec("PRAGMA query_only=ON;");
41
+ return {
42
+ exec: (sql) => db.exec(sql),
43
+ prepare(sql) {
44
+ const raw = db.prepare(sql);
45
+ return {
46
+ run: (...params) => raw.run(...params),
47
+ get: (...params) => plain(raw.get(...params)),
48
+ all: (...params) => raw.all(...params).map((r) => ({ ...r })),
49
+ };
50
+ },
51
+ transaction: (fn) => fn(),
52
+ close: () => db.close(),
53
+ };
54
+ }
37
55
  export function openDatabase(path) {
38
56
  const memory = path === ":memory:";
39
57
  const fresh = !memory && !existsSync(path);
@@ -5,6 +5,11 @@ export declare class SQLiteStore implements SessionStore {
5
5
  constructor(path: string);
6
6
  /** Forward-only, idempotent migrations. Existing databases keep all rows. */
7
7
  private migrate;
8
+ /**
9
+ * The root of a session (itself for roots), following `parent_id`. Unknown ids are their own
10
+ * root, so callers never throw on a session another process has not written yet.
11
+ */
12
+ rootOf(id: string): string;
8
13
  create(workspace: string, provider: string, model: string): Session;
9
14
  private row;
10
15
  get(id: string): Session;
@@ -112,6 +112,81 @@ export class SQLiteStore {
112
112
  this.db.exec("ALTER TABLE workspaces ADD COLUMN archived_at INTEGER");
113
113
  this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(5)");
114
114
  });
115
+ if (!has(6))
116
+ this.db.transaction(() => {
117
+ // v6 (additive): Python analysis executions, published artifacts, persisted capability
118
+ // grants (audit included) and tabular datasets (used from the data phase on).
119
+ this.db.exec(`CREATE TABLE IF NOT EXISTS analysis_executions(
120
+ id TEXT PRIMARY KEY,
121
+ session TEXT NOT NULL,
122
+ root_session TEXT NOT NULL,
123
+ workspace TEXT NOT NULL,
124
+ run_id TEXT, call_id TEXT,
125
+ runtime TEXT NOT NULL CHECK(runtime IN ('managed','oci')),
126
+ status TEXT NOT NULL CHECK(status IN ('running','completed','failed','cancelled','timed_out')),
127
+ script_sha256 TEXT NOT NULL,
128
+ exit_code INTEGER, error TEXT,
129
+ rel_dir TEXT NOT NULL,
130
+ rerun_of TEXT,
131
+ created_at INTEGER NOT NULL, ended_at INTEGER);
132
+ CREATE INDEX IF NOT EXISTS analysis_executions_session
133
+ ON analysis_executions(root_session, created_at);
134
+ CREATE TABLE IF NOT EXISTS artifacts(
135
+ id TEXT PRIMARY KEY,
136
+ session TEXT NOT NULL, root_session TEXT NOT NULL, workspace TEXT NOT NULL,
137
+ run_id TEXT, call_id TEXT,
138
+ execution_id TEXT REFERENCES analysis_executions(id),
139
+ source_path TEXT,
140
+ title TEXT NOT NULL, file_name TEXT NOT NULL,
141
+ kind TEXT NOT NULL, mime TEXT NOT NULL,
142
+ bytes INTEGER NOT NULL, file_count INTEGER NOT NULL DEFAULT 1,
143
+ sha256 TEXT NOT NULL, entry TEXT,
144
+ rel_dir TEXT NOT NULL,
145
+ status TEXT NOT NULL CHECK(status IN ('ready','deleted','expired')),
146
+ provenance TEXT NOT NULL,
147
+ created_at INTEGER NOT NULL, deleted_at INTEGER,
148
+ UNIQUE(execution_id, source_path));
149
+ CREATE INDEX IF NOT EXISTS artifacts_session ON artifacts(root_session, created_at);
150
+ CREATE TABLE IF NOT EXISTS capability_grants(
151
+ id TEXT PRIMARY KEY,
152
+ capability TEXT NOT NULL,
153
+ session TEXT NOT NULL,
154
+ workspace TEXT NOT NULL,
155
+ scope TEXT NOT NULL CHECK(scope IN ('once','session')),
156
+ decision TEXT NOT NULL CHECK(decision IN ('allow','deny')),
157
+ source TEXT NOT NULL CHECK(source IN ('tui','web','flag','headless-grant')),
158
+ call_id TEXT, run_id TEXT, correlation_id TEXT,
159
+ created_at INTEGER NOT NULL,
160
+ revoked_at INTEGER, revoked_by TEXT);
161
+ CREATE INDEX IF NOT EXISTS capability_grants_lookup
162
+ ON capability_grants(capability, session, scope, decision, revoked_at);
163
+ CREATE TABLE IF NOT EXISTS datasets(
164
+ id TEXT PRIMARY KEY,
165
+ session TEXT NOT NULL, root_session TEXT NOT NULL,
166
+ name TEXT NOT NULL, format TEXT NOT NULL,
167
+ sha256 TEXT NOT NULL, bytes INTEGER NOT NULL,
168
+ blob_hash TEXT,
169
+ source_path TEXT,
170
+ db_rel_path TEXT NOT NULL,
171
+ ingest_version INTEGER NOT NULL,
172
+ sheets TEXT NOT NULL,
173
+ created_at INTEGER NOT NULL);
174
+ INSERT OR IGNORE INTO schema_migrations VALUES(6);`);
175
+ });
176
+ }
177
+ /**
178
+ * The root of a session (itself for roots), following `parent_id`. Unknown ids are their own
179
+ * root, so callers never throw on a session another process has not written yet.
180
+ */
181
+ rootOf(id) {
182
+ let current = id;
183
+ for (let depth = 0; depth < 32; depth++) {
184
+ const row = this.db.prepare("SELECT parent_id FROM sessions WHERE id=?").get(current);
185
+ if (!row?.parent_id)
186
+ return current;
187
+ current = row.parent_id;
188
+ }
189
+ return current;
115
190
  }
116
191
  create(workspace, provider, model) {
117
192
  const id = crypto.randomUUID();
@@ -0,0 +1,24 @@
1
+ export type ZipEntry = {
2
+ name: string;
3
+ source: string;
4
+ } | {
5
+ name: string;
6
+ data: Uint8Array;
7
+ };
8
+ /** CRC-32 (IEEE) of `bytes`, continuing from `crc`. */
9
+ export declare function crc32(bytes: Uint8Array, crc?: number): number;
10
+ /** Something to write the archive to (an HTTP response, a file stream…), honoring backpressure. */
11
+ export interface ZipSink {
12
+ write(chunk: Buffer): boolean;
13
+ once(event: "drain", listener: () => void): unknown;
14
+ }
15
+ /**
16
+ * Writes `entries` as a ZIP archive to `out`, streaming file contents. Entry names use `/` and
17
+ * must be relative; the caller validates them. Throws when the archive would need ZIP64.
18
+ */
19
+ export declare function writeZip(entries: ZipEntry[], out: ZipSink, options?: {
20
+ now?: Date;
21
+ signal?: AbortSignal;
22
+ }): Promise<{
23
+ bytes: number;
24
+ }>;