@elie-laloum/outpost 1.1.2 → 1.1.4

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 (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +19 -191
  3. package/dist/cli/scaffold.constants.js +1 -0
  4. package/dist/cli/scaffold.constants.js.map +1 -1
  5. package/dist/infrastructure/binary-process.d.ts +3 -0
  6. package/dist/infrastructure/binary-process.js +53 -0
  7. package/dist/infrastructure/binary-process.js.map +1 -0
  8. package/dist/infrastructure/binary-process.types.d.ts +5 -0
  9. package/dist/infrastructure/binary-process.types.js +2 -0
  10. package/dist/infrastructure/binary-process.types.js.map +1 -0
  11. package/dist/infrastructure/transfer-copy.d.ts +3 -0
  12. package/dist/infrastructure/transfer-copy.js +51 -0
  13. package/dist/infrastructure/transfer-copy.js.map +1 -0
  14. package/dist/providers/container-archive.d.ts +3 -0
  15. package/dist/providers/container-archive.js +27 -0
  16. package/dist/providers/container-archive.js.map +1 -0
  17. package/dist/providers/container-command.js +3 -3
  18. package/dist/providers/container-command.js.map +1 -1
  19. package/dist/providers/container-files.d.ts +2 -1
  20. package/dist/providers/container-files.js +100 -11
  21. package/dist/providers/container-files.js.map +1 -1
  22. package/dist/providers/container.js +11 -2
  23. package/dist/providers/container.js.map +1 -1
  24. package/package.json +9 -6
  25. package/README.fr.md +0 -251
  26. package/ROADMAP.md +0 -34
  27. package/docs/api.md +0 -147
  28. package/docs/architecture.fr.md +0 -37
  29. package/docs/architecture.md +0 -57
  30. package/docs/migration-1.1.fr.md +0 -44
  31. package/docs/migration-1.1.md +0 -38
  32. package/docs/operations.md +0 -101
  33. package/docs/providers.md +0 -119
  34. package/docs/workflows.md +0 -101
@@ -1,16 +1,105 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { dirname, posix } from "node:path";
3
- export function containerFiles(call, name) {
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, mkdtemp } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join, posix, resolve } from "node:path";
5
+ import { invariant, OutpostError } from "../domain/errors.js";
6
+ import { quote } from "../infrastructure/process.js";
7
+ import { copyTransfer, removeTransferDirectory, transferDestination, } from "../infrastructure/transfer-copy.js";
8
+ import { transfer } from "../infrastructure/transfer.js";
9
+ import { containerArchive } from "./container-archive.js";
10
+ export function containerFiles(runtime, archive = containerArchive) {
11
+ const { call, name } = runtime;
4
12
  return {
5
- async upload(source, destination, options = {}) {
6
- options.signal?.throwIfAborted();
7
- await call(["exec", name, "mkdir", "-p", posix.dirname(destination)], options);
8
- await call(["cp", source, `${name}:${destination}`], options);
13
+ upload(source, destination, options = {}) {
14
+ return transfer(options, async (signal) => {
15
+ if (runtime.isClosed())
16
+ throw new OutpostError("provider", "Container sandbox is closed");
17
+ const absolute = resolve(source);
18
+ const scratch = `/tmp/outpost-transfer-${randomUUID()}`;
19
+ const entry = basename(absolute);
20
+ invariant(entry, "Upload a directory's contents using a non-root path");
21
+ const contents = /[\\/]\.$/.test(source);
22
+ await call(["exec", name, "mkdir", "-m", "700", scratch], { signal });
23
+ try {
24
+ await archive(runtime, {
25
+ executable: "tar",
26
+ arguments: ["-cf", "-", "-C", dirname(absolute), "--", entry],
27
+ }, {
28
+ executable: "tar",
29
+ arguments: [
30
+ "-xpf",
31
+ "-",
32
+ "--ignore-zeros",
33
+ "--no-same-owner",
34
+ "-C",
35
+ scratch,
36
+ ],
37
+ }, true, signal);
38
+ await call([
39
+ "exec",
40
+ name,
41
+ "sh",
42
+ "-c",
43
+ `mkdir -p ${quote(posix.dirname(destination))} && cp -a -- ${quote(posix.join(scratch, entry) + (contents ? "/." : ""))} ${quote(destination)}`,
44
+ ], { signal });
45
+ }
46
+ finally {
47
+ await call([
48
+ "exec",
49
+ name,
50
+ "sh",
51
+ "-c",
52
+ `chmod -R u+rwX ${quote(scratch)} && rm -rf -- ${quote(scratch)}`,
53
+ ]);
54
+ }
55
+ });
9
56
  },
10
- async download(source, destination, options = {}) {
11
- options.signal?.throwIfAborted();
12
- await mkdir(dirname(destination), { recursive: true });
13
- await call(["cp", `${name}:${source}`, destination], options);
57
+ download(source, destination, options = {}) {
58
+ return transfer(options, async (signal) => {
59
+ if (runtime.isClosed())
60
+ throw new OutpostError("provider", "Container sandbox is closed");
61
+ await transferDestination(destination);
62
+ const absolute = posix.resolve(runtime.root, source);
63
+ const entry = posix.basename(absolute);
64
+ invariant(entry, "Download a directory's contents using a non-root path");
65
+ const scratch = await mkdtemp(join(tmpdir(), "outpost-transfer-"));
66
+ try {
67
+ await archive(runtime, {
68
+ executable: "tar",
69
+ arguments: [
70
+ "-xpf",
71
+ "-",
72
+ "--ignore-zeros",
73
+ "--no-same-owner",
74
+ "-C",
75
+ scratch,
76
+ ],
77
+ }, {
78
+ executable: "tar",
79
+ arguments: [
80
+ "-cf",
81
+ "-",
82
+ "-C",
83
+ posix.dirname(absolute),
84
+ "--",
85
+ entry,
86
+ ],
87
+ }, false, signal);
88
+ const existing = await lstat(destination).catch((cause) => {
89
+ if (cause.code === "ENOENT")
90
+ return undefined;
91
+ throw cause;
92
+ });
93
+ const target = existing?.isDirectory() && !source.endsWith("/.")
94
+ ? join(destination, entry)
95
+ : destination;
96
+ await mkdir(dirname(target), { recursive: true });
97
+ await copyTransfer(join(scratch, entry), target, signal);
98
+ }
99
+ finally {
100
+ await removeTransferDirectory(scratch);
101
+ }
102
+ });
14
103
  },
15
104
  };
16
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"container-files.js","sourceRoot":"","sources":["../../src/providers/container-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAI3C,MAAM,UAAU,cAAc,CAC5B,IAA8B,EAC9B,IAAY;IAEZ,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,EAAE;YAC5C,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;YACjC,MAAM,IAAI,CACR,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,EACzD,OAAO,CACR,CAAC;YACF,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,WAAW,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;QAChE,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,EAAE;YAC9C,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;YACjC,MAAM,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACvD,MAAM,IAAI,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,MAAM,EAAE,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAChE,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { mkdir } from \"node:fs/promises\";\nimport { dirname, posix } from \"node:path\";\nimport type { SandboxLease } from \"../domain/sandbox.types.ts\";\nimport type { ContainerRuntime } from \"./container.types.ts\";\n\nexport function containerFiles(\n call: ContainerRuntime[\"call\"],\n name: string,\n): Pick<SandboxLease, \"upload\" | \"download\"> {\n return {\n async upload(source, destination, options = {}) {\n options.signal?.throwIfAborted();\n await call(\n [\"exec\", name, \"mkdir\", \"-p\", posix.dirname(destination)],\n options,\n );\n await call([\"cp\", source, `${name}:${destination}`], options);\n },\n async download(source, destination, options = {}) {\n options.signal?.throwIfAborted();\n await mkdir(dirname(destination), { recursive: true });\n await call([\"cp\", `${name}:${source}`, destination], options);\n },\n };\n}\n"]}
1
+ {"version":3,"file":"container-files.js","sourceRoot":"","sources":["../../src/providers/container-files.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { lstat, mkdir, mkdtemp } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, dirname, join, posix, resolve } from \"node:path\";\nimport type { SandboxLease } from \"../domain/sandbox.types.ts\";\nimport { invariant, OutpostError } from \"../domain/errors.ts\";\nimport { quote } from \"../infrastructure/process.ts\";\nimport {\n copyTransfer,\n removeTransferDirectory,\n transferDestination,\n} from \"../infrastructure/transfer-copy.ts\";\nimport { transfer } from \"../infrastructure/transfer.ts\";\nimport { containerArchive } from \"./container-archive.ts\";\nimport type { ContainerRuntime } from \"./container.types.ts\";\n\nexport function containerFiles(\n runtime: ContainerRuntime,\n archive = containerArchive,\n): Pick<SandboxLease, \"upload\" | \"download\"> {\n const { call, name } = runtime;\n return {\n upload(source, destination, options = {}) {\n return transfer(options, async (signal) => {\n if (runtime.isClosed())\n throw new OutpostError(\"provider\", \"Container sandbox is closed\");\n const absolute = resolve(source);\n const scratch = `/tmp/outpost-transfer-${randomUUID()}`;\n const entry = basename(absolute);\n invariant(entry, \"Upload a directory's contents using a non-root path\");\n const contents = /[\\\\/]\\.$/.test(source);\n await call([\"exec\", name, \"mkdir\", \"-m\", \"700\", scratch], { signal });\n try {\n await archive(\n runtime,\n {\n executable: \"tar\",\n arguments: [\"-cf\", \"-\", \"-C\", dirname(absolute), \"--\", entry],\n },\n {\n executable: \"tar\",\n arguments: [\n \"-xpf\",\n \"-\",\n \"--ignore-zeros\",\n \"--no-same-owner\",\n \"-C\",\n scratch,\n ],\n },\n true,\n signal,\n );\n await call(\n [\n \"exec\",\n name,\n \"sh\",\n \"-c\",\n `mkdir -p ${quote(posix.dirname(destination))} && cp -a -- ${quote(posix.join(scratch, entry) + (contents ? \"/.\" : \"\"))} ${quote(destination)}`,\n ],\n { signal },\n );\n } finally {\n await call([\n \"exec\",\n name,\n \"sh\",\n \"-c\",\n `chmod -R u+rwX ${quote(scratch)} && rm -rf -- ${quote(scratch)}`,\n ]);\n }\n });\n },\n download(source, destination, options = {}) {\n return transfer(options, async (signal) => {\n if (runtime.isClosed())\n throw new OutpostError(\"provider\", \"Container sandbox is closed\");\n await transferDestination(destination);\n const absolute = posix.resolve(runtime.root, source);\n const entry = posix.basename(absolute);\n invariant(\n entry,\n \"Download a directory's contents using a non-root path\",\n );\n const scratch = await mkdtemp(join(tmpdir(), \"outpost-transfer-\"));\n try {\n await archive(\n runtime,\n {\n executable: \"tar\",\n arguments: [\n \"-xpf\",\n \"-\",\n \"--ignore-zeros\",\n \"--no-same-owner\",\n \"-C\",\n scratch,\n ],\n },\n {\n executable: \"tar\",\n arguments: [\n \"-cf\",\n \"-\",\n \"-C\",\n posix.dirname(absolute),\n \"--\",\n entry,\n ],\n },\n false,\n signal,\n );\n const existing = await lstat(destination).catch(\n (cause: NodeJS.ErrnoException) => {\n if (cause.code === \"ENOENT\") return undefined;\n throw cause;\n },\n );\n const target =\n existing?.isDirectory() && !source.endsWith(\"/.\")\n ? join(destination, entry)\n : destination;\n await mkdir(dirname(target), { recursive: true });\n await copyTransfer(join(scratch, entry), target, signal);\n } finally {\n await removeTransferDirectory(scratch);\n }\n });\n },\n };\n}\n"]}
@@ -101,7 +101,7 @@ export function containerProvider(engine, options = {}, executor = executeProces
101
101
  name,
102
102
  "sh",
103
103
  "-c",
104
- "command -v setsid >/dev/null && command -v kill >/dev/null",
104
+ "command -v setsid >/dev/null && command -v kill >/dev/null && command -v tar >/dev/null && command -v cp >/dev/null",
105
105
  ]);
106
106
  }
107
107
  catch (cause) {
@@ -134,7 +134,16 @@ export function containerProvider(engine, options = {}, executor = executeProces
134
134
  call,
135
135
  isClosed: () => closed,
136
136
  }),
137
- ...containerFiles(call, name),
137
+ ...containerFiles({
138
+ engine,
139
+ config,
140
+ executor,
141
+ root,
142
+ name,
143
+ env,
144
+ call,
145
+ isClosed: () => closed,
146
+ }),
138
147
  release,
139
148
  };
140
149
  },
@@ -1 +1 @@
1
- {"version":3,"file":"container.js","sourceRoot":"","sources":["../../src/providers/container.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD,OAAO,EACL,cAAc,EACd,KAAK,EACL,cAAc,GACf,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAK7D,MAAM,UAAU,SAAS,CAAC,UAAkB;IAC1C,OAAO,WACL,QAAQ,CAAC,UAAU,CAAC;SACjB,WAAW,EAAE;SACb,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,IAAI,WACtC,EAAE,CAAC;AACL,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,MAAuB,EACvB,UAA4B,EAAE,EAC9B,WAAqB,cAAc,EACnC,WAA4B,OAAO,CAAC,QAAQ;IAE5C,MAAM,MAAM,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;IAC9B,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAC3B,SAAS,CACP,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,EAC/C,uBAAuB,CACxB,CAAC;IACJ,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;QAC/B,SAAS,CACP,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC;YACnC,MAAM,CAAC,QAAQ,IAAI,iBAAiB,CAAC,eAAe,EACtD,8BAA8B,CAC/B,CAAC;IACJ,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,SAAS,EAAE,SAAS;QACpB,SAAS,EAAE,EAAE,GAAG,MAAM,CAAC,SAAS,EAAE;QAClC,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,WAAW,UAAU,EAAE,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YAC5D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI;gBAC1B,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,iBAAiB,CAAC,GAAG;gBAChD,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,iBAAiB,CAAC,GAAG;aACjD,CAAC;YACF,SAAS,CACP,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;gBACxB,IAAI,CAAC,GAAG,IAAI,CAAC;gBACb,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;gBAC1B,IAAI,CAAC,GAAG,IAAI,CAAC,EACf,wBAAwB,CACzB,CAAC;YACF,MAAM,IAAI,GAAG,CAAC,IAAuB,EAAE,QAA0B,EAAE,EAAE,EAAE,CACrE,cAAc,CACZ;gBACE,UAAU,EAAE,MAAM;gBAClB,SAAS,EAAE,IAAI;gBACf,UAAU,EAAE,iBAAiB,CAAC,UAAU;gBACxC,GAAG,KAAK;aACT,EACD,QAAQ,CACT,CAAC;YACJ,MAAM,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YACtE,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,EACjC,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC;YAChC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,eAAe,CACzD,OAAO,EACP,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,IAAI,CACL,CAAC;YACF,MAAM,IAAI,GAAG,aAAa,CAAC;gBACzB,MAAM;gBACN,MAAM;gBACN,IAAI;gBACJ,IAAI;gBACJ,IAAI;gBACJ,KAAK;gBACL,OAAO;gBACP,WAAW;aACZ,CAAC,CAAC;YACH,IAAI,MAAM,GAAG,KAAK,EAChB,SAAoC,CAAC;YACvC,IAAI,UAAU,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;YAC1B,MAAM,OAAO,GAAG,GAAkB,EAAE;gBAClC,MAAM,GAAG,IAAI,CAAC;gBACd,SAAS,KAAK,IAAI,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;qBACxC,IAAI,CAAC,GAAG,EAAE;oBACT,UAAU,EAAE,CAAC;gBACf,CAAC,CAAC;qBACD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;oBACf,SAAS,GAAG,SAAS,CAAC;oBACtB,MAAM,KAAK,CAAC;gBACd,CAAC,CAAC,CAAC;gBACL,OAAO,SAAS,CAAC;YACnB,CAAC,CAAC;YACF,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,EAAE;oBACf,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACtD,CAAC,CAAC;gBACH,MAAM,IAAI,CACR,CAAC,OAAO,EAAE,IAAI,CAAC,EACf,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CACjD,CAAC;gBACF,KAAK,MAAM,MAAM,IAAI,WAAW;oBAC9B,MAAM,IAAI,CAAC;wBACT,MAAM;wBACN,QAAQ;wBACR,KAAK;wBACL,IAAI;wBACJ,IAAI;wBACJ,IAAI;wBACJ,YAAY,KAAK,CAAC,MAAM,CAAC,aAAa,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE;qBAC9E,CAAC,CAAC;gBACL,IAAI,WAAW,CAAC,IAAI;oBAClB,MAAM,IAAI,CAAC;wBACT,MAAM;wBACN,QAAQ;wBACR,KAAK;wBACL,IAAI;wBACJ,OAAO;wBACP,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;wBACzB,IAAI;qBACL,CAAC,CAAC;gBACL,MAAM,IAAI,CAAC;oBACT,MAAM;oBACN,IAAI;oBACJ,IAAI;oBACJ,IAAI;oBACJ,4DAA4D;iBAC7D,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC;oBACH,MAAM,OAAO,EAAE,CAAC;gBAClB,CAAC;gBAAC,OAAO,OAAO,EAAE,CAAC;oBACjB,MAAM,IAAI,cAAc,CACtB,CAAC,KAAK,EAAE,OAAO,CAAC,EAChB,yCAAyC,CAC1C,CAAC;gBACJ,CAAC;gBACD,MAAM,KAAK,CAAC;YACd,CAAC;YACD,UAAU,GAAG,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE;gBACzC,IAAI,CAAC,MAAM;oBACT,YAAY,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE;wBAC5C,OAAO,EAAE,iBAAiB,CAAC,SAAS;wBACpC,KAAK,EAAE,QAAQ;wBACf,WAAW,EAAE,IAAI;qBAClB,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;YACH,OAAO;gBACL,IAAI;gBACJ,IAAI;gBACJ,MAAM,EAAE,gBAAgB,CAAC;oBACvB,MAAM;oBACN,MAAM;oBACN,QAAQ;oBACR,IAAI;oBACJ,IAAI;oBACJ,GAAG;oBACH,IAAI;oBACJ,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM;iBACvB,CAAC;gBACF,GAAG,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC;gBAC7B,OAAO;aACR,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { basename } from \"node:path\";\nimport type { Command } from \"../domain/command.types.ts\";\nimport { invariant } from \"../domain/errors.ts\";\nimport type { SandboxProvider } from \"../domain/sandbox.types.ts\";\nimport {\n executeProcess,\n quote,\n requireSuccess,\n} from \"../infrastructure/process.ts\";\nimport type { Executor } from \"../infrastructure/process.types.ts\";\nimport { registerCleanup } from \"../infrastructure/shutdown.ts\";\nimport { containerCommand } from \"./container-command.ts\";\nimport { containerFiles } from \"./container-files.ts\";\nimport { containerMounts } from \"./container-mounts.ts\";\nimport { containerPlan } from \"./container-plan.ts\";\nimport { containerPreflight } from \"./container-preflight.ts\";\nimport { containerDefaults } from \"./container.constants.ts\";\nimport type { ContainerEngine, ContainerOptions } from \"./container.types.ts\";\n\nexport type { ContainerOptions } from \"./container.types.ts\";\n\nexport function imageName(repository: string): string {\n return `outpost:${\n basename(repository)\n .toLowerCase()\n .replace(/[^a-z0-9_.-]/g, \"-\") || \"workspace\"\n }`;\n}\n\nexport function containerProvider(\n engine: ContainerEngine,\n options: ContainerOptions = {},\n executor: Executor = executeProcess,\n platform: NodeJS.Platform = process.platform,\n): SandboxProvider {\n const config = { ...options };\n if (config.cpus !== undefined)\n invariant(\n Number.isFinite(config.cpus) && config.cpus > 0,\n \"cpus must be positive\",\n );\n if (config.memoryMb !== undefined)\n invariant(\n Number.isSafeInteger(config.memoryMb) &&\n config.memoryMb >= containerDefaults.minimumMemoryMb,\n \"memoryMb must be at least 64\",\n );\n return {\n name: engine,\n placement: \"mounted\",\n variables: { ...config.variables },\n async acquire(context) {\n context.signal?.throwIfAborted();\n const name = `outpost-${randomUUID()}`;\n const image = config.image ?? imageName(context.repository);\n const user = config.user ?? {\n uid: process.getuid?.() ?? containerDefaults.uid,\n gid: process.getgid?.() ?? containerDefaults.gid,\n };\n invariant(\n Number.isInteger(user.uid) &&\n user.uid >= 0 &&\n Number.isInteger(user.gid) &&\n user.gid >= 0,\n \"Invalid container user\",\n );\n const call = (args: readonly string[], extra: Partial<Command> = {}) =>\n requireSuccess(\n {\n executable: engine,\n arguments: args,\n deadlineMs: containerDefaults.deadlineMs,\n ...extra,\n },\n executor,\n );\n await containerPreflight(engine, platform, call, config, image, user);\n const root = containerDefaults.root,\n home = containerDefaults.home;\n const { env, volumes, fileParents } = await containerMounts(\n context,\n config,\n platform,\n root,\n home,\n );\n const args = containerPlan({\n config,\n engine,\n user,\n root,\n name,\n image,\n volumes,\n fileParents,\n });\n let closed = false,\n releasing: Promise<void> | undefined;\n let unregister = () => {};\n const release = (): Promise<void> => {\n closed = true;\n releasing ??= call([\"rm\", \"--force\", name])\n .then(() => {\n unregister();\n })\n .catch((error) => {\n releasing = undefined;\n throw error;\n });\n return releasing;\n };\n try {\n await call(args, {\n ...(context.signal ? { signal: context.signal } : {}),\n });\n await call(\n [\"start\", name],\n context.signal ? { signal: context.signal } : {},\n );\n for (const parent of fileParents)\n await call([\n \"exec\",\n \"--user\",\n \"0:0\",\n name,\n \"sh\",\n \"-c\",\n `mkdir -p ${quote(parent)} && chown ${user.uid}:${user.gid} ${quote(parent)}`,\n ]);\n if (fileParents.size)\n await call([\n \"exec\",\n \"--user\",\n \"0:0\",\n name,\n \"chown\",\n `${user.uid}:${user.gid}`,\n home,\n ]);\n await call([\n \"exec\",\n name,\n \"sh\",\n \"-c\",\n \"command -v setsid >/dev/null && command -v kill >/dev/null\",\n ]);\n } catch (cause) {\n try {\n await release();\n } catch (cleanup) {\n throw new AggregateError(\n [cause, cleanup],\n \"Sandbox provisioning and cleanup failed\",\n );\n }\n throw cause;\n }\n unregister = registerCleanup(release, () => {\n if (!closed)\n execFileSync(engine, [\"rm\", \"--force\", name], {\n timeout: containerDefaults.cleanupMs,\n stdio: \"ignore\",\n windowsHide: true,\n });\n });\n return {\n root,\n home,\n invoke: containerCommand({\n engine,\n config,\n executor,\n root,\n name,\n env,\n call,\n isClosed: () => closed,\n }),\n ...containerFiles(call, name),\n release,\n };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"container.js","sourceRoot":"","sources":["../../src/providers/container.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { basename } from \"node:path\";\nimport type { Command } from \"../domain/command.types.ts\";\nimport { invariant } from \"../domain/errors.ts\";\nimport type { SandboxProvider } from \"../domain/sandbox.types.ts\";\nimport {\n executeProcess,\n quote,\n requireSuccess,\n} from \"../infrastructure/process.ts\";\nimport type { Executor } from \"../infrastructure/process.types.ts\";\nimport { registerCleanup } from \"../infrastructure/shutdown.ts\";\nimport { containerCommand } from \"./container-command.ts\";\nimport { containerFiles } from \"./container-files.ts\";\nimport { containerMounts } from \"./container-mounts.ts\";\nimport { containerPlan } from \"./container-plan.ts\";\nimport { containerPreflight } from \"./container-preflight.ts\";\nimport { containerDefaults } from \"./container.constants.ts\";\nimport type { ContainerEngine, ContainerOptions } from \"./container.types.ts\";\n\nexport type { ContainerOptions } from \"./container.types.ts\";\n\nexport function imageName(repository: string): string {\n return `outpost:${\n basename(repository)\n .toLowerCase()\n .replace(/[^a-z0-9_.-]/g, \"-\") || \"workspace\"\n }`;\n}\n\nexport function containerProvider(\n engine: ContainerEngine,\n options: ContainerOptions = {},\n executor: Executor = executeProcess,\n platform: NodeJS.Platform = process.platform,\n): SandboxProvider {\n const config = { ...options };\n if (config.cpus !== undefined)\n invariant(\n Number.isFinite(config.cpus) && config.cpus > 0,\n \"cpus must be positive\",\n );\n if (config.memoryMb !== undefined)\n invariant(\n Number.isSafeInteger(config.memoryMb) &&\n config.memoryMb >= containerDefaults.minimumMemoryMb,\n \"memoryMb must be at least 64\",\n );\n return {\n name: engine,\n placement: \"mounted\",\n variables: { ...config.variables },\n async acquire(context) {\n context.signal?.throwIfAborted();\n const name = `outpost-${randomUUID()}`;\n const image = config.image ?? imageName(context.repository);\n const user = config.user ?? {\n uid: process.getuid?.() ?? containerDefaults.uid,\n gid: process.getgid?.() ?? containerDefaults.gid,\n };\n invariant(\n Number.isInteger(user.uid) &&\n user.uid >= 0 &&\n Number.isInteger(user.gid) &&\n user.gid >= 0,\n \"Invalid container user\",\n );\n const call = (args: readonly string[], extra: Partial<Command> = {}) =>\n requireSuccess(\n {\n executable: engine,\n arguments: args,\n deadlineMs: containerDefaults.deadlineMs,\n ...extra,\n },\n executor,\n );\n await containerPreflight(engine, platform, call, config, image, user);\n const root = containerDefaults.root,\n home = containerDefaults.home;\n const { env, volumes, fileParents } = await containerMounts(\n context,\n config,\n platform,\n root,\n home,\n );\n const args = containerPlan({\n config,\n engine,\n user,\n root,\n name,\n image,\n volumes,\n fileParents,\n });\n let closed = false,\n releasing: Promise<void> | undefined;\n let unregister = () => {};\n const release = (): Promise<void> => {\n closed = true;\n releasing ??= call([\"rm\", \"--force\", name])\n .then(() => {\n unregister();\n })\n .catch((error) => {\n releasing = undefined;\n throw error;\n });\n return releasing;\n };\n try {\n await call(args, {\n ...(context.signal ? { signal: context.signal } : {}),\n });\n await call(\n [\"start\", name],\n context.signal ? { signal: context.signal } : {},\n );\n for (const parent of fileParents)\n await call([\n \"exec\",\n \"--user\",\n \"0:0\",\n name,\n \"sh\",\n \"-c\",\n `mkdir -p ${quote(parent)} && chown ${user.uid}:${user.gid} ${quote(parent)}`,\n ]);\n if (fileParents.size)\n await call([\n \"exec\",\n \"--user\",\n \"0:0\",\n name,\n \"chown\",\n `${user.uid}:${user.gid}`,\n home,\n ]);\n await call([\n \"exec\",\n name,\n \"sh\",\n \"-c\",\n \"command -v setsid >/dev/null && command -v kill >/dev/null && command -v tar >/dev/null && command -v cp >/dev/null\",\n ]);\n } catch (cause) {\n try {\n await release();\n } catch (cleanup) {\n throw new AggregateError(\n [cause, cleanup],\n \"Sandbox provisioning and cleanup failed\",\n );\n }\n throw cause;\n }\n unregister = registerCleanup(release, () => {\n if (!closed)\n execFileSync(engine, [\"rm\", \"--force\", name], {\n timeout: containerDefaults.cleanupMs,\n stdio: \"ignore\",\n windowsHide: true,\n });\n });\n return {\n root,\n home,\n invoke: containerCommand({\n engine,\n config,\n executor,\n root,\n name,\n env,\n call,\n isClosed: () => closed,\n }),\n ...containerFiles({\n engine,\n config,\n executor,\n root,\n name,\n env,\n call,\n isClosed: () => closed,\n }),\n release,\n };\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elie-laloum/outpost",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Programmable sandboxes, coding agents and typed workflows.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,9 +40,6 @@
40
40
  "files": [
41
41
  "dist",
42
42
  "README.md",
43
- "README.fr.md",
44
- "docs",
45
- "ROADMAP.md",
46
43
  "CHANGELOG.md",
47
44
  "SECURITY.md",
48
45
  "LICENSE"
@@ -60,7 +57,12 @@
60
57
  "prepack": "npm run build",
61
58
  "test:package": "node scripts/package-smoke.mjs",
62
59
  "test:container": "node --test test/container.test.ts",
63
- "architecture": "node scripts/check-architecture.mjs"
60
+ "architecture": "node scripts/check-architecture.mjs",
61
+ "docs:sync": "npm run build && node docs/scripts/sync-reference.mjs",
62
+ "docs:check": "node docs/scripts/sync-reference.mjs --check && npm run check --prefix docs",
63
+ "docs:dev": "npm run dev --prefix docs",
64
+ "docs:build": "npm run build --prefix docs",
65
+ "docs:test": "npm run test:build --prefix docs && node docs/scripts/check-examples.mjs && node --test docs/scripts/release-channel.test.mjs"
64
66
  },
65
67
  "repository": {
66
68
  "type": "git",
@@ -92,5 +94,6 @@
92
94
  "c8": "^10.1.3",
93
95
  "prettier": "^3.5.3",
94
96
  "typescript": "^6.0.3"
95
- }
97
+ },
98
+ "homepage": "https://elie-laloum.github.io/outpost/"
96
99
  }
package/README.fr.md DELETED
@@ -1,251 +0,0 @@
1
- # Outpost
2
-
3
- [English](README.md) · [API](docs/api.md) · [Fournisseurs](docs/providers.md) · [Workflows](docs/workflows.md) · [Exploitation](docs/operations.md) · [Roadmap](ROADMAP.md)
4
-
5
- Outpost est une bibliothèque TypeScript pour exécuter des agents de développement dans des sandbox réutilisables, gérer leurs espaces de travail Git et composer des workflows typés. La v1 fournit les adaptateurs **Codex** et **Claude Code**, ainsi que les environnements Docker, Podman, Vercel, Daytona et une exécution locale explicite. Les adaptateurs d’agents et les fournisseurs de sandbox sont deux points d’extension distincts.
6
-
7
- [GitLab](https://gitlab.elielaloum.com/elielaloum/outpost) est le dépôt principal. [GitHub](https://github.com/elie-laloum/outpost) reçoit le miroir et exécute la CI ainsi que les publications de packages.
8
-
9
- ## Prérequis
10
-
11
- - Node.js **24 ou supérieur**, Git et un dépôt possédant au moins un commit.
12
- - Docker ou Podman pour l’isolation locale ; un compte et le SDK optionnel correspondant pour une sandbox cloud.
13
- - Une authentification native de l’agent ou sa clé API. Un jeton GitHub/GitLab ne remplace pas une clé de modèle.
14
- - Des imports ESM. Les déclarations TypeScript sont livrées ; Node exécute directement les scripts générés en `.ts`/`.mts`.
15
-
16
- ## Installation et premier lancement
17
-
18
- Depuis les sources :
19
-
20
- ```sh
21
- npm ci
22
- npm run build
23
- npm pack
24
- ```
25
-
26
- Installez l’archive obtenue dans votre dépôt cible, ou utilisez [GitHub Packages](https://github.com/elie-laloum/outpost/packages). La configuration du registre figure dans le [guide d’exploitation](docs/operations.md).
27
-
28
- ```sh
29
- npm install --save-dev /chemin/elie-laloum-outpost-1.1.2.tgz
30
- npx outpost init --yes --agent codex --provider docker --template blank --build
31
- ```
32
-
33
- Copiez `.outpost/.env.example` vers `.outpost/.env`. Renseignez `OPENAI_API_KEY`, ou laissez cette déclaration vide pour reprendre sa valeur dans le processus. Pour Claude, choisissez `--agent claude` et déclarez `ANTHROPIC_API_KEY` ou `CLAUDE_CODE_OAUTH_TOKEN`. Seules les clés déclarées sont importées dans les sandbox isolés.
34
-
35
- ```sh
36
- node .outpost/run.mts "Ajouter la validation de configuration et ses tests"
37
- ```
38
-
39
- Le script s’appelle `run.ts` si le projet déclare `"type": "module"`, sinon `run.mts`. L’initialisation refuse d’écraser des fichiers existants. `--install` installe la bibliothèque et le SDK optionnel avec le gestionnaire détecté : npm, pnpm, yarn ou bun. Sans `--build`, construisez ensuite l’image avec `npx outpost image build`.
40
-
41
- ## Exécution ponctuelle
42
-
43
- ```ts
44
- import { dispatch, codex } from "@elie-laloum/outpost";
45
- import { docker } from "@elie-laloum/outpost/providers/docker";
46
-
47
- const result = await dispatch({
48
- agent: codex(),
49
- provider: docker(),
50
- branch: { mode: "integrate" },
51
- brief: { text: "Corriger les tests en échec, vérifier et créer un commit." },
52
- });
53
-
54
- console.log(result.branch, result.commits, result.conversation);
55
- ```
56
-
57
- `dispatch` crée l’environnement, lance l’agent, récupère les modifications et la conversation native, intègre les commits si demandé, puis ferme les ressources dont il est propriétaire. Le travail non commité est conservé. En cas d’échec, les worktrees séparés restent disponibles pour inspection.
58
-
59
- | Politique Git | Comportement |
60
- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
61
- | `{ mode: "current" }` | Utilise le checkout actuel ; défaut des fournisseurs montés et locaux. |
62
- | `{ mode: "named", name: "feature/validation", from: "main" }` | Crée ou réutilise un worktree géré pour cette branche. |
63
- | `{ mode: "integrate", from: "main" }` | Crée une branche temporaire puis fusionne ses commits dans la branche hôte d’origine. |
64
-
65
- Les fournisseurs distants exigent `named` ou `integrate`. `from` est optionnel. Une branche déjà utilisée ailleurs produit une erreur explicite. L’intégration refuse un changement de branche hôte et conserve le worktree en cas de conflit.
66
-
67
- ## Sandbox réutilisable
68
-
69
- ```ts
70
- import { createSandbox, claude } from "@elie-laloum/outpost";
71
-
72
- await using sandbox = await createSandbox({
73
- agent: claude({ model: "sonnet", reasoning: "high" }),
74
- branch: { mode: "named", name: "feature/settings" },
75
- });
76
-
77
- const first = await sandbox.dispatch({
78
- brief: { text: "Implémenter la validation." },
79
- });
80
- const tests = await sandbox.command({ executable: "npm", arguments: ["test"] });
81
- if (tests.status !== 0)
82
- await first.resume({ brief: { text: "Corriger les tests." } });
83
- await sandbox.attach();
84
- ```
85
-
86
- Une commande retourne son statut, même non nul. Une exécution d’agent en échec lève une erreur. L’annulation par `AbortSignal`, le délai maximal et la surveillance d’inactivité arrêtent l’opération courante sans détruire la sandbox réutilisable. Une sandbox accepte une opération active à la fois. Le parallélisme utilise plusieurs sandbox.
87
-
88
- L’agent par défaut est optionnel pour une sandbox chaude. Fournissez `agent` à chaque appel de `sandbox.dispatch` ou `sandbox.attach` pour changer d’agent ou de modèle sans recréer l’environnement. Chaque exécution reçoit uniquement les variables de l’adaptateur choisi.
89
-
90
- `await using` ferme automatiquement le handle. `close()` est également disponible et idempotent. `attach()` ouvre le terminal natif avec Docker, Podman ou le fournisseur local. Les fournisseurs cloud rejettent explicitement cette opération.
91
-
92
- ## Espace de travail indépendant
93
-
94
- ```ts
95
- import { openWorkspace, codex, claude } from "@elie-laloum/outpost";
96
-
97
- await using workspace = await openWorkspace({
98
- branch: { mode: "named", name: "feature/shared-work" },
99
- copies: [".env.test"],
100
- });
101
-
102
- await workspace.dispatch({
103
- agent: codex(),
104
- brief: { text: "Implémenter et commiter." },
105
- });
106
- await workspace.dispatch({
107
- agent: claude(),
108
- brief: { text: "Relire, tester et commiter les corrections." },
109
- });
110
- ```
111
-
112
- L’espace de travail fourni survit aux sandbox successives. Son propriétaire doit le fermer après elles. Un worktree contenant des modifications reste sur disque ; `close()` retourne alors `retainedDirectory`. Un worktree propre est supprimé, mais une branche nommée reste disponible. `close({ preserve: true })` conserve aussi un worktree propre.
113
-
114
- `workspace.sandbox()` et `workspace.attach()` donnent accès aux autres cycles de vie. Avec une sandbox chaude en mode `integrate`, utilisez explicitement `sandbox.workspace.integrate()` au moment voulu. Les exécutions ponctuelles intègrent avant de retourner leur résultat.
115
-
116
- ## Prompts et boucles
117
-
118
- Un `brief` contient exactement une source : `text` littéral, ou `file` avec des `values` primitives optionnelles. Les fichiers sont relus à chaque passage ; le texte littéral n’est jamais interprété.
119
-
120
- ```ts
121
- const result = await dispatch({
122
- agent: codex(),
123
- brief: {
124
- file: ".outpost/brief.md",
125
- values: { OBJECTIVE: "Corriger la validation" },
126
- },
127
- passes: 5,
128
- until: ["<outpost>done</outpost>"],
129
- idleMs: 600_000,
130
- settleMs: 60_000,
131
- });
132
- ```
133
-
134
- Les fichiers acceptent `{{OBJECTIVE}}`, les variables réservées `{{WORK_BRANCH}}` et `{{BASE_BRANCH}}`, ainsi que des commandes comme `` !`git status --short` ``. Les commandes présentes dans le fichier d’origine s’exécutent en parallèle dans la sandbox après les hooks. Une valeur substituée ne peut pas introduire une nouvelle commande d’expansion. Une valeur insérée dans une commande existante conserve toutefois sa sémantique shell : utilisez des valeurs fiables ou correctement échappées.
135
-
136
- Les variables absentes déclenchent une erreur ; les variables inutilisées passent par `warn`. Un marqueur de fin interrompt la boucle. Si l’agent reste actif après ce marqueur, le délai de grâce se renouvelle à chaque sortie, puis arrête sa commande. Un budget épuisé sans marqueur retourne `completed: false`.
137
-
138
- Chaque tour fournit sa durée, son statut, son texte, sa conversation éventuelle et les tokens bruts. Chaque tour expose aussi son chemin `transcript` lorsqu’il est capturé. Le résultat regroupe `input`, `cached` (lecture du cache), `cacheCreated` (création du cache), `output`, les commits et le marqueur observé. Aucun coût monétaire n’est extrapolé.
139
-
140
- ## Réponses structurées
141
-
142
- ```ts
143
- import { response } from "@elie-laloum/outpost";
144
-
145
- const result = await dispatch({
146
- agent: codex(),
147
- brief: {
148
- text: "Analyser le dépôt et répondre avec <report>un JSON contenant ok</report>.",
149
- },
150
- response: response.json({
151
- tag: "report",
152
- repairs: 2,
153
- schema(input) {
154
- if (
155
- !input ||
156
- typeof input !== "object" ||
157
- !("ok" in input) ||
158
- typeof input.ok !== "boolean"
159
- ) {
160
- throw new Error("Un booléen ok est requis");
161
- }
162
- return { ok: input.ok };
163
- },
164
- }),
165
- });
166
-
167
- console.log(result.value.ok);
168
- ```
169
-
170
- `response.text` extrait du texte balisé. `response.json` accepte une fonction ou un validateur Standard Schema, y compris asynchrone. La balise d’ouverture doit être demandée dans le prompt. Les réponses structurées exigent un seul passage ; les tentatives de correction reprennent la même conversation. En cas d’échec, `ResponseError.recovery` contient les informations de reprise disponibles.
171
-
172
- ## Conversations natives
173
-
174
- ```ts
175
- await result.resume({ brief: { text: "Expliquer le résultat." } });
176
- await result.fork({ brief: { text: "Explorer une autre solution." } });
177
- ```
178
-
179
- La capture est active par défaut. Outpost enregistre les transcriptions dans le stockage natif de l’agent sur l’hôte et réécrit les champs de répertoire de travail. Le contenu des messages n’est pas remplacé globalement. Les transcriptions des sous-agents Claude sont récupérées au mieux, avec avertissement en cas d’échec. Un échec de capture de la transcription principale fait échouer l’exécution.
180
-
181
- `saveConversations: false` désactive la capture sur l’adaptateur. Une reprise ponctuelle vérifie d’abord l’existence de la transcription hôte. `fork` crée une nouvelle conversation ; il faut aussi choisir un autre workspace pour isoler les fichiers. `conversationHome` permet de choisir un autre répertoire de stockage hôte.
182
-
183
- ## Workflows typés
184
-
185
- ```ts
186
- import { task, workflow } from "@elie-laloum/outpost";
187
-
188
- const inspect = task({
189
- key: "inspect",
190
- perform: async () => ({ ready: true }),
191
- });
192
- const implement = task({
193
- key: "implement",
194
- after: [inspect],
195
- retry: { attempts: 2, delayMs: 500 },
196
- perform: async (context) => context.value(inspect).ready,
197
- });
198
-
199
- const result = await workflow("delivery", [inspect, implement]).start({
200
- concurrency: 2,
201
- });
202
- result.unwrap();
203
- console.log(result.value(implement));
204
- ```
205
-
206
- Les dépendances absentes, doublons et cycles sont rejetés avant exécution. Les valeurs sont typées et propres à chaque lancement. Conditions, tentatives supplémentaires, limites de concurrence, événements d’observation et diagrammes Mermaid sont disponibles. `agentTask`, `commandTask` et `isolatedTask` relient le graphe aux opérations de sandbox.
207
-
208
- L’annulation et le délai maximal des tâches sont coopératifs : le code d’une tâche doit respecter `context.signal`. Le planificateur attend le nettoyage des tâches actives avant de retourner. Les opérations Outpost respectent ce signal. Une fonction JavaScript arbitraire qui l’ignore ne peut pas être arrêtée de force.
209
-
210
- Les cinq modèles CLI sont `blank`, `iterate`, `review`, `plan` et `plan-review`. Les deux derniers font travailler plusieurs analyses indépendantes avant l’implémentation. Les connecteurs GitHub Issues, Beads et personnalisés sont générés avec `--tracker`. Consultez le [guide des workflows](docs/workflows.md).
211
-
212
- Un `dispatch({ passes })` ponctuel acquiert une nouvelle sandbox à chaque passe ; `sandbox.dispatch({ passes })` conserve son environnement. Les fichiers de prompt relatifs sont résolus depuis le répertoire de l’appelant. Les méthodes `resume`/`fork` d’un résultat ponctuel acceptent une nouvelle branche, un autre fournisseur et des hooks ; celles d’un résultat chaud restent liées à leur sandbox.
213
-
214
- ## Campagnes par issue
215
-
216
- `campaign` relit le backlog à chaque cycle, valide un plan typé et attribue une branche par issue. Le parallélisme est borné ; la revue partage la sandbox d’implémentation. Une phase de fusion et de vérification intervient même pour une seule branche. La fermeture d’une issue a lieu uniquement après intégration des commits dans la branche hôte. GitHub et Beads fournissent les opérations de liste, détail et fermeture. Un connecteur personnalisé implémente `Backlog`.
217
-
218
- Les starters exposent `cycles`, `concurrency`, `implementationPasses`, `reviewPasses`, les agents de chaque rôle et les règles de `.outpost/STANDARDS.md`. Une issue sans commit n’est ni revue ni fermée. Un cycle sans progression s’arrête. `recoveryDetails(error)` fournit les chemins disponibles en cas d’erreur, sans remplacer la raison d’annulation. [Guide de migration 1.1 en français](docs/migration-1.1.fr.md).
219
-
220
- ## Configuration et fournisseurs
221
-
222
- Les fournisseurs prennent en charge les variables d’environnement, montages de fichiers/répertoires, réseaux, ressources, groupes supplémentaires, périphériques et labels SELinux selon le backend. Les SDK cloud sont des dépendances optionnelles, chargées uniquement à l’utilisation du fournisseur concerné. [Référence des fournisseurs](docs/providers.md).
223
-
224
- Les hooks s’exécutent après la copie des entrées puis après la création de la sandbox. Les groupes `hostReady` et `sandboxReady` tournent en parallèle ; les commandes hôte restent séquentielles, tandis que les commandes du groupe sandbox démarrent ensemble et annulent leurs voisines en cas d’échec.
225
-
226
- Seul `.outpost/.env` est lu. Une valeur non vide du fichier est prioritaire ; une déclaration vide reprend la valeur du processus. Le `.env` à la racine du dépôt n’est pas importé. Les variables explicites du fournisseur et de l’adaptateur restent prioritaires. Une clé déclarée simultanément par le fournisseur et l’adaptateur provoque une erreur. Les journaux sont écrits dans `.outpost/logs` par défaut ; `false`, `"stdout"`, un chemin personnalisé et le mode verbeux sont disponibles.
227
-
228
- `AgentAdapter` permet d’ajouter un agent. `SandboxProvider`, `mountedProvider` et `remoteProvider` permettent d’ajouter un backend. Le domaine ne dépend pas d’un SDK cloud. [Architecture](docs/architecture.md) et [API complète](docs/api.md).
229
-
230
- ## Vérification et publication
231
-
232
- ```sh
233
- npm ci
234
- npm run check
235
- npm run coverage
236
- npm run test:package
237
- ```
238
-
239
- La CI GitHub exécute les tests unitaires et fonctionnels sous Linux, Windows et macOS, impose 80 % de couverture des lignes, fonctions et branches, installe le package réellement produit et teste Docker/Podman. Les tests de contrats cloud utilisent des doubles contrôlés ; les essais avec de vrais modèles et comptes cloud nécessitent leurs identifiants.
240
-
241
- Les tags de version sont créés dans GitLab puis répliqués. Le workflow de release valide le tag et la CI, publie GitHub Packages et joint l’archive installable à une release GitHub. La publication npm est optionnelle, via trusted publishing une fois configuré. [Procédure d’exploitation](docs/operations.md).
242
-
243
- ## Limites d’isolation et récupération
244
-
245
- Docker et Podman montent le worktree sélectionné et les métadonnées Git. L’agent peut modifier ce dépôt et son état Git. Les autres chemins hôte ne sont pas exposés par défaut, mais ce montage ne constitue pas une protection contre un agent hostile visant le dépôt partagé. `local()` s’exécute directement sur votre machine, sans isolation.
246
-
247
- Les fournisseurs cloud synchronisent l’historique, les changements suivis et les fichiers nouveaux. Les identifiants et auteurs des commits sont conservés. Une modification hôte concurrente déclenche une erreur de récupération plutôt qu’un écrasement. Les bundles, patches et fichiers de secours sont conservés sous `.outpost/recovery`.
248
-
249
- Les logs, transcriptions et fichiers de récupération peuvent contenir des données privées. Les dossiers d’exécution et `.env` sont exclus du versionnement. [Détails de sécurité](SECURITY.md).
250
-
251
- Licence MIT : [LICENSE](LICENSE).
package/ROADMAP.md DELETED
@@ -1,34 +0,0 @@
1
- # Roadmap
2
-
3
- ## V1 contract
4
-
5
- Reusable sandboxes; independent workspaces; current/named/integration branch policies; Claude Code and Codex adapters; native session capture, resume and fork; Docker, Podman, host, Vercel and Daytona providers; command streaming and cancellation; file prompts, expansion, loops and structured responses; setup hooks; typed workflows; CLI initialization; EN/FR documentation; cross-platform tests and package release automation.
6
-
7
- ## V1.x: efficiency and diagnostics
8
-
9
- - Durable campaign closure receipts to reconcile tracker outages without reimplementing merged issues.
10
- - Configurable planner policies, merge validation commands and per-role token budgets.
11
-
12
- - Incremental remote file manifests and compressed transfer batches while preserving recovery guarantees.
13
- - Content-addressed dependency caches and prebuilt agent images with signed provenance.
14
- - Configurable recovery retention with an inspect/prune CLI and storage quotas.
15
- - Structured metrics, OpenTelemetry integration and per-workflow usage budgets.
16
- - Richer diagnostic reports for provider capabilities and agent CLI compatibility.
17
- - More hosted-provider contract fixtures and scheduled live compatibility checks.
18
-
19
- ## V2: durable orchestration
20
-
21
- - Persisted workflow checkpoints and recovery after host restarts.
22
- - Distributed task queues and worker leases with fencing tokens.
23
- - Explicit approvals and pause/resume nodes in workflows.
24
- - Artifact contracts between isolated tasks, with typed validation and lineage.
25
- - Native terminal transport for cloud backends where reliable PTY APIs are available.
26
- - Additional agent adapters through the existing domain ports.
27
-
28
- ## Research
29
-
30
- - Stronger Git metadata isolation for adversarial agents.
31
- - MicroVM backends and fine-grained outbound network policies.
32
- - Speculative execution with conflict-aware integration and cost limits.
33
-
34
- Roadmap items are additions to the v1 contract. They are not prerequisites for running the current library.