Docs/Cookbook

环境服务(EaaS)

为你的 agent 租用一台隔离执行环境:warm pool 策略、创建、exec、文件、事件订阅与生命周期——TypeScript / Python / CLI 三端同输入可复现。

⏱ 预计时间: 15 min

概览

环境服务(EaaS)是一台可租用的隔离执行环境。你将:

1.

给项目挂 warm pool 策略(可选,operator 业务)

2.

从官方模板创建环境

3.

等就绪,然后跑命令、交换文件

4.

订阅事件流看它干活

5.

pause / wake / delete——平台异步回收载体

下面每个面都给出三个可互换的客户端(TypeScript SDK、Python SDK、CLI)。同一输入下它们发出相同的 wire 请求;按你的技术栈选一个。某个客户端有意不承载的面会显式说明,不会静默跳过。

前置

租户 + 项目。 在 dashboard 开租户,随附一个默认项目。所有计费 EaaS 状态都挂在 (tenant, project) 上。

一把 key:

sk-prismer-live-… —— platform key(dashboard → API keys)。解析到你的 operator 身份,带完整 operator scope 集(含 warm-pool:write)。本教程用它走通。

sk-eaas-… —— project 机器 key。只有机器面 scope(environments:、exec:、files:*、usage:read、events:read),有意不含 warm-pool scope——策略是 operator 业务,不是机器业务。

ps-eaas-… —— principal session token,在 POST /api/v1/sessions 为单个环境内的终端用户换发。受限 principal 面见 API 参考。

一个模板。 官方 registry 目前提供 node20-web(profile 2c4g):一台在 8080 端口应答的 Node 20 载体。接受 name@sha256:… pin 且服务端校验(不符 → 422 template_unavailable)。

所有调用用 Authorization: Bearer 。下面没有任何一步需要把 tenant secret 放进你的进程。

Step 1 —— Warm pool 策略(可选)

warm pool 在项目上常备就绪环境,让后续 create 从分钟级降到秒级。策略是项目上的 desired state,每次 PATCH 都是整体替换——省略字段回落到 0 / cold,所以先读当前策略、写回合并后的完整策略。读到的 revision 作为 If-Match 头(裸整数)发回;并发改动会得到 412 revision_conflict。

import { PrismerClient } from '@prismer/sdk';

const client = new PrismerClient({
  baseUrl: 'https://prismer.cloud',
  apiKey: process.env.PRISMER_API_KEY!, // sk-prismer-live-…(operator)
});

// 读 desired 策略 + 池有效态 + 成本窗口。
const pool = await client.projects.warmPool.get(projectId);
if (!pool.success) throw new Error(`${pool.error.code}: ${pool.error.message}`);
console.log('desired', pool.data.desired, 'at revision', pool.data.revision);

// 整体替换,If-Match = 刚读到的 revision。
const patched = await client.projects.warmPool.patch(
  projectId,
  {
    policy: {
      minReady: 0,
      maxReady: 1,
      idleRetentionSeconds: 300,
      dailyBudgetCredits: '5.000',
      onMiss: 'cold',
    },
  },
  { revision: pool.data.revision },
);

onMiss 是池未命中时的裁决,客户端绝不替你改写。 cold(默认)按需冷启;fail 在池空时原样返回 503 warm_capacity_unavailable。两个 SDK 都不会重试这个 503,也不会把 fail 改写成 cold——要降级请显式做;要安全重试写面,请传同一个 Idempotency-Key(新 key 就是新操作)。

Step 2 —— 创建环境

create 幂等键控,返回 202 + 初始状态投影(state: pending,随后 provisioning)。同 key 重放拿到同一台环境;同 key 换 body 是 409 idempotency_conflict。

const created = await client.environments.create(
  {
    projectId,
    template: 'node20-web', // 或 'node20-web@sha256:…' 钉住版本
    profile: '2c4g',
    ttlSeconds: 3600, // 60..86400
    metadata: { source: 'docs' },
    // env: { MY_SECRET: '…' },  // 身份绑定后注入,不写事件、不进日志
  },
  { idempotencyKey: 'docs-create-1' }, // 可选;省略时自动生成
);
if (!created.success) throw new Error(created.error.code);
const envId = created.data.environmentId;
console.log(created.data.state, created.data.revision, created.data.epoch);

REST 响应经 Location 头返回 canonical URL(/api/v1/environments/{id})。

Step 3 —— 等就绪

状态投影暴露 readiness 三元组(sandbox / services / agent)和 milestones 日志。readiness.sandbox 是「载体已起来」的信号,任何拓扑都能交付;readiness.services 要等模板 service 探针通过,前提是探针能从 cloud 侧到达 pod(云部署可以;本地 kind 没有 in-cluster DNS 时会保持 false,环境落 degraded——exec 和文件仍然可用)。

SDK 还提供 waitUntilReady(id, { capability: 'services' });capability: 'agent' 会立即拒绝(capability_unavailable),因为 agent readiness 尚不是探针驱动。

let status = created.data;
for (;;) {
  if (status.readiness.sandbox) break;
  await new Promise((r) => setTimeout(r, 2000));
  const next = await client.environments.get(envId);
  if (!next.success) throw new Error(next.error.code);
  status = next.data;
}
console.log(status.state, status.startupPath); // running | degraded,cold | warm | wake | restore

Step 4 —— 跑命令

exec 在同步窗内跑命令。服务端返回 200 + exited 视图,或 202 + running 句柄,用 getExec 按 stdout 字节偏移分页读回。

const run = await client.environments.exec(
  envId,
  { command: ['sh', '-c', 'printf hello-from-eaas'], timeoutMs: 10000 },
  { idempotencyKey: 'docs-exec-1' },
);
if (!run.success) throw new Error(run.error.code);
if (run.data.status === 'exited') {
  console.log(run.data.exitCode, run.data.stdout);
} else {
  const later = await client.environments.getExec(envId, run.data.execId);
  if (later.success) console.log(later.data.status, later.data.stdout);
}

Step 5 —— 写文件、读文件

文件面是裸字节:PUT 用 application/octet-stream(≤ 1 MB),应答 size + sha256;GET 返回字节,摘要放在 X-Eaas-File-Sha256 响应头。在载体内同一文件位于 /home/agent/workspace/,所以在里面 sha256sum 应与 API 报告一致。

const bytes = new TextEncoder().encode('hello eaas\n');
const put = await client.environments.putFile(envId, 'notes/hello.txt', bytes);
if (put.success) console.log(put.data.size, put.data.sha256);

const got = await client.environments.getFile(envId, 'notes/hello.txt');
if (got.success) console.log(got.sha256, new TextDecoder().decode(got.bytes));

Step 6 —— 订阅事件

平台对环境做的每件事都会进一条 durable 的租户级事件流。两种 transport,一套交付契约:

SSE:GET /api/v1/events,Accept: text/event-stream——key 走 Authorization 头,绝不进 URL。

WebSocket:wss://…/ws/eaas/v1——首帧 {"type":"auth","token":"…"},再 {"type":"subscribe","cursor":"…","environmentIds":["…"]}。

两边都先按 cursor replay 再转实时,按 eventId 去重,cursor 不可续传时发 eaas.resync 信号(拉新快照后重订)。TypeScript SDK 自带该契约的订阅器;Python SDK 与 CLI 还没有封装流——直接用裸 endpoint。

import { connectEaasEvents } from '@prismer/sdk'; // 也可 '@prismer/sdk/environment-stream'

const handle = await connectEaasEvents({
  baseUrl: 'https://prismer.cloud',
  token: process.env.PRISMER_API_KEY!,
  transport: 'sse', // 或 'ws'
  filters: { environmentIds: [envId] },
  onEvent: (event) => console.log(event.type, event.cursor, event.payload),
  onResync: () => console.log('cursor 被 prune——重读状态后重订'),
  onEnd: (info) => console.log('stream ended', info), // 带最后 cursor 重连
});

// 之后:handle.disconnect();

Step 7 —— 生命周期

pause 冻结计算(暂停期间 files 与 exec 拒绝),wake 唤回,suspend / delete 退役——delete 先到 stopping,平台异步回收载体到 stopped。四个动词都幂等键控。expiresAt 过点的环境即使不删除也会拒绝新工作。

await client.environments.pause(envId, { idempotencyKey: 'docs-pause-1' });
await client.environments.wake(envId);
await client.environments.delete(envId, { idempotencyKey: 'docs-delete-1' });

错误面

每个面都返回同一种 envelope:{ success: true, data, requestId } 或 { success: false, error: { code, message, details }, requestId }。code 是封闭集合——invalid_policy / invalid_request(400)、invalid_token(401)、budget_exhausted(402)、scope_denied(403)、not_owned(404——环境不存在与别人的环境刻意不可区分)、state_conflict / idempotency_conflict(409)、revision_conflict(412)、template_unavailable / capability_unavailable(422)、quota_exceeded / rate_limited(429)、warm_capacity_unavailable / provider_unavailable / pricing_unavailable(503)。三个客户端都原样透出该 code;传输级故障(无路由、断连)是客户端合成 code,有意不做跨端统一。principal-session 面(POST /api/v1/sessions 与 ps-eaas-… Bearer 面)带自己的 401 code——publishable_key_invalid / invalid_session / session_expired。

密钥轮换与到期

机器 key 无 TTL——轮换是「签新、留旧、再吊销」,窗口内两把 key 同时对同一租户有效(双活),切换调用方无空窗。principal session token 相反:永远带到期(ttlSeconds,60..604800),过期即 401 session_expired。key 签发与吊销是 operator 管理动作;上面的机器面自身不轮换任何东西。

下一步

API 参考:environments——全部面、参数与错误码。

warm pool 成本与 usage 面在 dashboard 租户视图。

事件 envelope、cursor 与 resync 语义见 events API 参考。