Vault and ingestion — bRRAIn Docs

Read, write, append, list, move, copy and delete vault files with the Platform SDK, and commit incoming content through governed ingestion.

Vault and ingestion

Knowledge in bRRAIn is files in the vault: Markdown with YAML frontmatter, JSON, PDFs, Office documents, images. The SDK gives you two ways to put bytes there:

| Path | Use it for | What the pod applies | | --- | --- | --- | | Vault().Write | Your extension's own state, logs, caches and generated files in extensions/<slug>/ | Token and namespace check, path validation, atomic write | | Ingestion().Commit | Content arriving from outside: uploads, attachments, connector payloads, agent output, anything entering the shared knowledge base | All of the above plus the organization's ingestion rule (size limit), the security gate, downstream processing and new-file events |

Reading

| Method | Returns | | --- | --- | | ReadBytes(ctx, path) | The stored bytes. Use this for almost everything. | | Read(ctx, path) | The raw wire value (a base64 JSON string). Passing it straight to json.Unmarshal fails with "cannot unmarshal string". | | ReadRange(ctx, path, start, end) | The byte range [start, end), for previews of large files. 416 when out of bounds. | | Stat(ctx, path) | A *VaultEntry (Path, Name, IsDir, Size, ModTime, MIME); 404 when missing. | | List(ctx, dir) | The immediate children of a folder, folders first, then by name. One level only. |

A JSON state file with a first-run branch:

type State struct {
	LastRun string `json:"last_run"`
}

const statePath = "extensions/hello-notes/state.json"

func LoadState(ctx context.Context, c *platformsdk.Client) (State, error) {
	var st State
	body, err := c.Vault().ReadBytes(ctx, statePath)
	var apiErr *platformsdk.APIError
	if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
		return st, nil // first run
	}
	if err != nil {
		return st, err
	}
	return st, json.Unmarshal(body, &st)
}

Writing and appending

Vault().Write(ctx, VaultWriteRequest{Path, Content, Append}) creates missing parent folders and, for an overwrite, writes to a temporary file and renames it over the target, so readers see either the old content or the new content, never a mix.

func SaveState(ctx context.Context, c *platformsdk.Client, st State) error {
	buf, err := json.MarshalIndent(st, "", "  ")
	if err != nil {
		return err
	}
	return c.Vault().Write(ctx, platformsdk.VaultWriteRequest{Path: statePath, Content: buf})
}

func AppendLog(ctx context.Context, c *platformsdk.Client, line string) error {
	return c.Vault().Write(ctx, platformsdk.VaultWriteRequest{
		Path:    "extensions/hello-notes/run.log",
		Content: []byte(line + "\n"),
		Append:  true,
	})
}

Marshal first and write once. Building a file from several appends leaves a half-written file if the process stops between calls. A write does not run the organization's ingestion rule or ask the policy engine on your behalf.

Moving, copying and deleting

  • Move(ctx, VaultMoveRequest{From, To}) renames atomically. The platform may refuse a move across zones with 409.
  • Copy(ctx, VaultCopyRequest{From, To, WithMetadata}) duplicates a path; WithMetadata also copies Document Portal comments, labels and annotations.
  • Delete(ctx, VaultDeleteRequest{Path, Soft, Reason}): always set Soft explicitly. Soft: true moves the path to the organization's trash with your Reason recorded. Soft is a plain bool with omitempty, so leaving it unset sends nothing and the pod performs a permanent delete, including everything under a folder.

Wrap user-requested deletes in a policy check and an audit event; see Governance.

Governed ingestion

For every Ingestion().Commit, the pod looks up the organization's ingestion rule for your (OrgID, SourceSlug) pair, enforces its size limit (413), runs the security gate (403 with reasons), writes the file, applies the rule's follow-up processing and publishes new-file events so automations fire.

func IngestUpload(ctx context.Context, c *platformsdk.Client, orgID, userID, project, name string, body []byte) (*platformsdk.IngestionCommitResponse, error) {
	resp, err := c.Ingestion().Commit(ctx, platformsdk.IngestionCommitRequest{
		OrgID:       orgID,
		SourceSlug:  "hello-notes",
		RecordID:    "upload-" + name,
		TargetPath:  "projects/" + project + "/inbox/" + name,
		Content:     body,
		UserID:      userID,
		ProjectID:   project,
		ContentType: "text/markdown",
		Metadata:    map[string]string{"origin": "upload"},
	})
	var apiErr *platformsdk.APIError
	if errors.As(err, &apiErr) {
		switch apiErr.StatusCode {
		case http.StatusRequestEntityTooLarge:
			return nil, fmt.Errorf("%s exceeds the organization's ingestion limit: %w", name, err)
		case http.StatusForbidden:
			return nil, fmt.Errorf("%s blocked by security policy: %w", name, err)
		}
	}
	return resp, err
}
  • The SDK rejects an empty OrgID, SourceSlug, TargetPath or Content before sending (platformsdk: Commit requires org_id, and so on). Read the organization id from your configuration.
  • SourceSlug is your extension. Never borrow another source's slug to get a larger limit.
  • UserID is the person whose content this is.
  • 413 and 403 are answers, not faults. Tell the user in plain language and do not retry.
  • The response echoes the rule the pod applied (RouteCompiler, UpdateUserMC, UpdateProjectMC, UpdateInstitutMC), so you can tell the user what happens next.
  • Append: true streams into one file; the size limit applies to the resulting file, so roll over to a new file before it is reached.

| Status | Meaning | | --- | --- | | 400 | Missing fields or an invalid body | | 403 | Blocked by the security gate | | 413 | Over the rule's size limit | | 501 | The pod has no ingestion path wired | | 502 | The vault write itself failed |

Images and documents

Vault files are bytes, so PDFs, Office files and images read and write exactly like text: ReadBytes returns the original bytes and Stat reports the MIME type. Use ReadRange for previews of large files. To send an image or a PDF to a model, see Models.

Next