13 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 5b8400410d the task activity's three outcomes are pinned
ExecuteTaskActivity is the seam between the durable queue and whatever actually
does the work, and none of its three paths was covered.

The one worth stating is the first: with no executor registered the activity
COMPLETES the task with an acknowledgment rather than failing it. That is the
right call — a task submitted to a deployment running no worker should not retry
forever against something that will never run it — but it means "completed" can
mean "nobody did this", and a test that says so out loud is cheaper than working
that out from a dashboard.

The other two are the pass-through and the failure: the executor's own result is
returned unchanged rather than the task the activity was handed, and a failure
yields NO task plus an error wrapping the cause. Returning the input task beside
an error would record work that never ran as work that did.

taskExecutor is package state, so the helper here saves and restores it; a test
that leaks an executor changes every test that runs after it.

Verified by breaking both compiling ways — returning the task beside the error,
and %w -> %v — each caught by the assertion written for it.

plugins/tasks 36.2% -> 37.9%.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 10:41:01 -07:00
zeekay e4b1f3c75c a Base is addressed at /v1/bases, and Base stops running a second auth
Two things wore the name "platform" here, and neither is the PaaS at
platform.hanzo.ai. That collision is why "where are my Bases?" had no answer a
person could act on: one prefix, two products, and the one you reached was
whichever you happened to mean.

A Base is what an org gets — its own collections, its own files, its own key —
so the resource a client addresses is /v1/bases. Not /v1/orgs: the org is IAM's
noun and Base does not get a second one. Everything scoped to an org now hangs
off the Base it belongs to (/v1/bases/{id}/config, /creds, /customers), and
compliance moves to /v1/compliance, a sibling of /v1/iam rather than a child of
Base's own resource. The package is `org`, which seven of its files already
were. Nothing addressed the old prefix — no client, no UI, no manifest — so this
renames a surface rather than breaking one.

The auth proxy is DELETED, not renamed. Its five routes reshaped bodies and
forwarded to /v1/iam/login, /signup, /userinfo and /send-verification-code —
which the opaque /v1/iam/{path...} passthrough beside it already reaches. So
Base carried a hand-written login, signup and one-time-code flow whose only job
was to be a second way to reach IAM. Auth is IAM's, and one way is the whole
point of that rule.

The prefix test that already banned /api/ bans this too, so the name cannot come
back: it reads route LITERALS through go/ast, so a comment explaining the ban is
prose and a route is a route. Verified to fail on the exact regression.
2026-08-10 19:18:08 -07:00
zeekay 054d65663a they are Bases, one per org — the word tenant is gone
Base's own registry was already org-native in every method — OrgDB(orgID),
HasOrg(orgID), orgDir(orgID) — and only the type names said tenant. So this is
the vocabulary catching up with the code, not a change of behaviour.

	TenantRegistry -> Bases          app.Bases().OrgDB(org)
	TenantConfig   -> BasesConfig
	MultiTenant    -> PerOrg         it enables per-org databases; say that
	MULTI_TENANT   -> MULTI_BASE
	core/tenant.go -> core/bases.go

Tenant was the wrong noun twice over: it is the multi-tenancy word, and this
product's unit is the thing a customer creates per org — a Base. Identity here
is org, user, project, and nothing else.

The UI never said it; this was Go-side only. 64 packages green.

One test was collateral and is fixed rather than renamed: TestToHTTPHeader
asserts a pure snake_case-to-Header-Case transform, so rewriting its EXPECTED
value while its input stayed "tenant_id" made it assert something false. The case
is dropped — org_id already covers that transform.
2026-08-10 18:23:40 -07:00
zeekayandHanzo Dev 52de10a14c tasks: the embedded engine listens on a socket, not a port
tasks v1.52.4 replaced EmbedConfig.ZAPPort with Address, where a filesystem
path binds a unix socket. Nothing off this host dials the embedded engine, so
there is no port to allocate and none to collide over — two Base processes on
one machine used to fight over 9999 and the loser came up with no durable
engine at all, silently.

The default socket lives under the app's own DataDir rather than a shared temp
dir, so the collision cannot reappear one layer down: the path names whose it
is. TASKS_EMBED_ADDRESS overrides it and takes a host:port just as well, for a
deployment that does want a remote listener.

client.Options.HostPort became Address on the same release; the two dial sites
and the embed test move with it, and the test now runs over a socket.

luxfi/vm moves v1.3.1 -> v1.3.2, which is what cloud already runs. The pinned
v1.3.1 hash was not the one the checksum database holds for that tag, and its
go.mod hash was v1.3.2's — a moved tag, so the module could not be verified at
all. v1.3.2 verifies.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 23:03:48 -07:00
zooqueenandHanzo Dev 26a3353a99 docs: say IAM, not the vendor name it was never derived from
Hanzo IAM (hanzoai/iam) is original clean-room work — its LICENSE says so.
The third-party-derived identity server was hanzoai/iam-v1, and that is
retired. Comments here named the old vendor as if it were the IAM we ship,
which was simply false.

Every hit was a comment or doc. The behavior each one describes is real and
still described; only the name changed:

- envelope shape {status, msg, data} and status:"error"-at-HTTP-200 are
  attributed to IAM, which is what the fakes actually emulate
- the phone-normalization probe still explains why three shapes are tried
- the /v1 vs /api/* mount notes now state the rule directly: /v1 is Base's
  one external prefix. Outbound /api/* calls to the IAM server are its
  contract and are untouched.
- hanzo.id not accepting HTTP Basic is stated on its own

No identifiers, routes, env vars or wire fields changed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 17:12:32 -07:00
zeekayandHanzo Dev d582e283be tasks: drop a doc line describing a parameter that no longer exists
"clientForOrgVar extracts the first org from a variadic param" survived the
rename that removed both the name and the variadic. A comment that describes a
signature the code no longer has is worse than none — it is the one place a
reader trusts over the code.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 23:42:45 -07:00
zeekayandHanzo Dev 11eb5ff1ba tasks: an org is one value, not a variadic
`orgID ...string` on 17 methods. The concept is "one org, or none"; the type
said "any number of orgs", so ClaimTask(id, agent, "a", "b", "c") compiled and
silently honoured "a". A parameter that accepts a list it will ignore is a
parameter that cannot be read correctly.

Worse than what it permitted is what it CONCEALED. Omitting a variadic looks
like nothing at all, so "unscoped" was never written down anywhere — and three
call sites turned out to be unscoped without anyone having decided that:

  dependenciesMet  read dependency tasks unscoped from inside a scoped
                   operation. Left unscoped, now stated: a dependency is a
                   property of the task graph, not of tenancy, so a blocker in
                   another org still blocks.
  GetNextTask      signalled the claim unscoped though it had the org in hand.
                   Now passes it.
  apis/tasks.go    read back tasks unscoped that it had just CREATED with an
                   org, two statements earlier.

Now `org string`, one `scoped()` helper replacing seven copies of
`if len(orgID) > 0 && orgID[0] != ""`, and a named `allOrgs` so a call site says
which it meant instead of leaving a blank. The in-process scheduler and the
timeout sweep are genuinely unscoped and now say so.

Also corrects a claim I put in this file myself: the org clause is NOT an
authorization boundary. Every route reaching these methods is behind
RequireSuperuserAuth and the value comes from a caller-supplied X-Org-Id header
— it narrows what an already-privileged caller sees. A comment asserting a
security property that does not hold is worse than no comment, because someone
will lean on it.

And clientForOrgVar -> clientFor: the suffix described the calling convention of
its argument rather than anything about the client returned.

13 tests pass including both org-isolation cases; package green under -race.
(apis/ fails -race on TestCollectionsList both with and without this change —
pre-existing, unrelated.)

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 23:41:59 -07:00
zeekayandHanzo Dev e0799f36fb tasks: the query builder is the only way to write a query
Nine hand-built SQL statements in the task store, six of them the same statement
six times: set some columns, but only on a row in an acceptable starting state,
owned by the caller org, then work out what zero-rows-affected meant. The org
clause is an authorization boundary and it was copy-pasted six times — one copy
drifting is one caller mutating another org task.

They collapse into transition(), which takes the columns, the states it will
accept, and the error to report on conflict. Each caller is now three lines.

The header said "raw SQL for exactly-once semantics", and that was never the
reason. dbx Update builds the same single guarded UPDATE, and it accepts an
Expression as a column value (builder.go:228) — so even FailTask CASE columns,
which decide retry-or-fail from a column inside the same statement that writes
it, go through the builder. The retry condition is now written ONCE and reused
by every column, instead of six copies of "[[retryCount]] < [[maxRetries]]"
that could drift into disagreeing about which branch they are in.

AgentHasActiveTask had NO test. I rewrote it to a LIMIT 1 EXISTS probe and the
suite stayed green — because nothing exercised the case where the agent is
free. A probe returns no row when nothing matches and Row reports
sql.ErrNoRows, so "this agent is idle" — the answer the scheduler acts on most
— became an error. It is COUNT again, an aggregate always returns a row, and
TestAgentHasActiveTask now covers both directions plus agent scoping and the
pending/terminal states that must not read as active.

Suite green including -race.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 22:33:44 -07:00
zeekayandGitHub 9eb4214b97 feat(tasks): repoint durable plugin to cloud's gated tasks engine (#39)
The standalone tasksd is retired; cloud embeds the Hanzo Tasks engine and
exposes it on a cluster-reachable, IAM-gated ZAP listener (Embedded.ServeGated,
RequireIdentity, org-scoped per tasks CONTRACT §6). Repoint base's durable
plugin — used by every hanzo-base app (playground, platform, ...) — onto it:

- default DurableConfig.Address -> cloud.hanzo.svc:9999 (was the dead
  tasks.hanzo.ai:7233; deployments dialing tasks.hanzo.svc:7243 via
  TASKS_ADDRESS get repointed in universe)
- mint a client_credentials bearer from the app's own IAM identity (canonical
  IAM_CLIENT_ID/IAM_CLIENT_SECRET + IAM_ENDPOINT|IAM_URL) via oauth2
  clientcredentials and ride it on every dial as the tasks client Token; the
  gated engine org-scopes to the token owner. nil creds -> ungated dial
  (dev/loopback), fail-soft to SQLite as before
- RegisterNamespace on connect + per org in ClientForOrg (idempotent upsert) —
  the gated engine org-prefixes the store, so a namespace must be registered
  before ExecuteWorkflow

Deps: hanzoai/tasks v1.40.0 -> v1.48.0 (adds Options.Token over ZAP +
org-scoped completion). Repin re-tagged transitive luxfi modules off their
poisoned go.sum hashes to the next clean patch: zap v0.8.8 -> v0.8.12,
pq v1.0.3 -> v1.1.0 (age v1.5.1 already on main).
2026-07-04 21:39:25 -07:00
zeekay de2b89f0d1 logger: app.Logger() returns the shim interface, default to luxfi/log
core.App.Logger() now returns logger.Logger (the new shim interface)
instead of *slog.Logger, so every consumer talks to the same
backend-agnostic surface and stops pinning itself to log/slog. The raw
*slog.Logger is still reachable as App.SlogLogger() for the in-app
BatchHandler reload hook and the two tests that type-assert the
underlying handler — that's the only sanctioned escape hatch.

Plugins and network code that previously held *slog.Logger fields move
to luxlog.Logger (kms, vault, zap-transport, base-network/transport_zap,
network/archive_writer, tools/tasks/client). luxfi/zap's Logger field
was a pure transit dependency; we drop it from base call sites since
zaplib has its own default.

Remaining "log/slog" imports — 11 files — are confined to the
adapter/handler bridge (tools/logger and core/log_printer/base/app) and
to two tests that exercise the slog.Handler contract directly. Those
are the irreducible boundary where slog is the language of the
interface.
2026-05-15 04:04:26 -07:00
zeekay 5939cbaa0c logger: switch attr-helpers to variadic positional pairs
Every non-handler call site that wrapped a value in
slog.String/Int/Bool/Any/Float64/Uint64 was passing an Attr through the
variadic any... slot of (*slog.Logger).Info/Warn/Error/Debug; the same
slot accepts the bare "key", value pair with identical semantics. Drop
the wrapper so the surface is one level less indirect and so the next
two commits can swap (*slog.Logger) for a small Logger interface
without rewriting every attr expression as well.

23 files, 156 call sites collapsed; no behavior change. Five files —
tools/logger/log.go, tools/logger/batch_handler.go +_test, and
core/log_printer.go +_test — are the handler-implementation layer and
legitimately reference slog.Attr/Level/Group; they were skipped.
2026-05-15 03:50:49 -07:00
zeekay 062181edf3 feat(plugins/tasks): migrate to github.com/hanzoai/tasks/pkg/sdk
Every go.temporal.io/sdk/{client,worker,workflow,activity,temporal}
import swapped to github.com/hanzoai/tasks/pkg/sdk/<same>. The three
protobuf-accessor sites in durable.go rewrote to the native flat
shape: DescribeWorkflow returns *WorkflowExecutionInfo directly;
ListWorkflows takes (query, pageSize, nextPageToken). No shim
package, no compatibility layer.

go.mod: replace directive -> ../tasks pending v1.36.0 tag (removed
in the follow-up cleanup commit in hanzoai/tasks PR-F).

Import-purity: grep go.temporal.io/ plugins/tasks/ -> 0 hits.
go test ./plugins/tasks/... -> green.
2026-04-23 13:54:22 -07:00
zeekay 4641f13ae7 feat: Base platform — CRDT privacy, React admin, on-chain anchor SDK 2026-03-01 18:00:00 -08:00